Trying to test a component that fetches data in componentWillMount, calls setState() with the received data and renders a number of child components, like this:
export default class MyComponent extends React.Component {
constructor() {
super();
this.state = {
items: []
};
}
componentWillMount() {
Api.fetchItems().end((err, response) => {
if (response && response.ok) {
this.setState({ items: response.items });
}
});
}
render() {
return (
<div className="items-list">
{ this.state.items.map((item, i) =>
<Item item={ item } key={ i }/>
) }
</div>
);
}
}
The test is very basic and almost 1to1 taken from the example:
import sinon from 'sinon';
import { expect } from 'chai';
it('mounts correctly', () => {
sinon.spy(MyComponent.prototype, 'componentDidMount');
const wrapper = mount(<MyComponent />);
expect(MyComponent.prototype.componentDidMount.calledOnce).to.equal(true);
});
Unfortunately, the test fails:
TypeError: Attempted to wrap undefined property componentDidMount as function
at Object.wrapMethod (node_modules/sinon/lib/sinon/util/core.js:106:29)
at Object.spy (node_modules/sinon/lib/sinon/spy.js:41:26)
at Context.<anonymous> (MyComponent.spec.js:21:19)
Any ideas? It seems the component doesn't even mount at all, adding a expect(wrapper.hasClass('items-list')).toBe(true) fails as well.
Just for the record, spying on other methods works just as expected:
sinon.spy(Api, 'fetchItems');
mount(<MyComponent />);
expect(Api.fetchItems.calledOnce).to.be.true;
sinon can't stub over non-existent properties. If you add a componentDidMount() {} to your component, then it should work.
I think componentDidMount is a typo and @doque was indeed referring to componentWillMount, because I am getting the same error: TypeError: Attempted to wrap undefined property componentWillMount as function.
sinon.spy(App.prototype, 'componentWillMount');
I am mounting this way:
const rendered = mount(<App />, { context: { store: store }});
I am testing a component wrapped in a Redux Provider and if I log the App.prototype, componentWillMount is simply not there:
Connect {
shouldComponentUpdate: [Function: shouldComponentUpdate],
computeStateProps: [Function: computeStateProps],
configureFinalMapState: [Function: configureFinalMapState],
computeDispatchProps: [Function: computeDispatchProps],
configureFinalMapDispatch: [Function: configureFinalMapDispatch],
updateStatePropsIfNeeded: [Function: updateStatePropsIfNeeded],
updateDispatchPropsIfNeeded: [Function: updateDispatchPropsIfNeeded],
updateMergedPropsIfNeeded: [Function: updateMergedPropsIfNeeded],
isSubscribed: [Function: isSubscribed],
trySubscribe: [Function: trySubscribe],
tryUnsubscribe: [Function: tryUnsubscribe],
componentDidMount: [Function: componentDidMount],
componentWillReceiveProps: [Function: componentWillReceiveProps],
componentWillUnmount: [Function: componentWillUnmount],
clearCache: [Function: clearCache],
handleChange: [Function: handleChange],
getWrappedInstance: [Function: getWrappedInstance],
render: [Function: render],
componentWillUpdate: [Function: componentWillUpdate] }
The property is not there, so sinon is not seeing it. Am I missing something obvious? Is this even an Enzyme issue?
Looks like your <App /> is wrapped by a redux's connect HOC. The prototype you're seeing is for that. You would need to get the wrapped instanced from Connect or just import your component (without the connect() wrapper) and mock the props that redux is providing.
closing in favor of #472
@Aweary thanks for the reply! I am mocking what Redux gives me like this:
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
...
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
...
store = mockStore(storeStateMock); // where storeStateMock is literally an object that mocks my state
Then mount the component as described above, passing the store as the context. Everything else works just fine - testing props and children component. The componentWillMount test doesn't work though :/
I am having same error, while the test looks pretty straight forward :/ anyone?
it('componentDidMount() should be called once', () => {
// Setup
// Exercise
const spy = sinon.spy(AddComment.prototype, 'componentDidMount');
wrapper = mount(<AddComment />);
// Verify
expect(spy.calledOnce).to.equal(true);
// Clean up
spy.restore();
});
UPDATE: passed in spy as props and it works :)
const spy = sinon.spy(AddComment.prototype, 'componentDidMount');
wrapper = mount(<AddComment componentDidMount={spy} />);
@ignasBa that definitely shouldn't work unless your component has a bug. Can you share the implementation of AddComment?
@ljharb So yes, I am still struggling with it. The approach above only works when my componentDidMount() in AddComment looks like this:
componentDidMount () {
this.setState({ id: this.props.id })
};
When I write it as arrow function:
componentDidMount = () => {
this.setState({ id: this.props.id })
};
It stops working. Then I tried to change my test to:
`
// Setup
const instance = new AddComment();
spy = sinon.stub(instance, 'componentDidMount');
wrapper = mount(<AddComment />);
// Verify
expect(spy.calledOnce).to.equal(true);`
It actually wraps my function, but test doesn't pass, spy does not get called :/
You don't want that to be an arrow function, that should only be a class method, and you should be spying on the prototype method.
@ljharb thanks! thats good to know. You know any good read what methods should be class, and which arrow?
@ignasBa any method that is included in the React Component API should be a class method.
@ignasBa nothing should be an arrow function or a bound function except a thin wrapper around a class method, for the purpose of binding to this or an argument. 100% of your significant code should be in instance methods, not own properties.
You can use arrow functions if you're using the property initializer syntax provided by the class properties babel transform.
Even then, it's a bad idea, because that creates an own property on every instance, instead of delegating the majority of the execution to the shared, optimizable, instance method on the prototype.
Most helpful comment
sinoncan't stub over non-existent properties. If you add acomponentDidMount() {}to your component, then it should work.