First of all, I am following the Flux architecture.
I have an indicator that shows a number of seconds, ex: 30 seconds. Every one second it shows 1 second less, so 29, 28, 27 till 0. When arrives to 0, I clear the interval so it stops repeating. Moreover, I trigger an action. When this action gets dispatched, my store notifies me. So when this happens, I reset the interval to 30s and so on. Component looks like:
var Indicator = React.createClass({
mixins: [SetIntervalMixin],
getInitialState: function(){
return{
elapsed: this.props.rate
};
},
getDefaultProps: function() {
return {
rate: 30
};
},
propTypes: {
rate: React.PropTypes.number.isRequired
},
componentDidMount: function() {
MyStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
MyStore.removeChangeListener(this._onChange);
},
refresh: function(){
this.setState({elapsed: this.state.elapsed-1})
if(this.state.elapsed == 0){
this.clearInterval();
TriggerAnAction();
}
},
render: function() {
return (
<p>{this.state.elapsed}s</p>
);
},
/**
* Event handler for 'change' events coming from MyStore
*/
_onChange: function() {
this.setState({elapsed: this.props.rate}
this.setInterval(this.refresh, 1000);
}
});
module.exports = Indicator;
Component works as expected. Now, I want to test it with Jest. I know I can use renderIntoDocument, then I can setTimeout of 30s and check if my component.state.elapsed is equal to 0 (for example).
But, what I want to test here are different things. I want to test if refresh function is called . Moreover, I'd like to test that when my elapsed state is 0, it triggers my TriggerAnAction(). Ok, for the first thing I tried to do:
jest.dontMock('../Indicator');
describe('Indicator', function() {
it('waits 1 second foreach tick', function() {
var React = require('react/addons');
var Indicator = require('../Indicator.js');
var TestUtils = React.addons.TestUtils;
var Indicator = TestUtils.renderIntoDocument(
<Indicator />
);
expect(Indicator.refresh).toBeCalled();
});
});
But I receive the following error when writing npm test:
Throws: Error: toBeCalled() should be used on a mock function
I saw from ReactTestUtils a mockComponent function but given its explanation, I am not sure if it is what I need.
Ok, in this point, I am stuck. Can anybody give me some light on how to test that two things I mentioned above?
That's the test I am trying to run (see comments in some lines):
jest.dontMock('../Indicator');
describe('Indicator', function() {
it('waits 1 second foreach tick', function() {
var React = require('react/addons');
var Indicator = require('../Indicator.js');
var TestUtils = React.addons.TestUtils;
var refresh = jest.genMockFunction();
Indicator.refresh = refresh;
var onChange = jest.genMockFunction();
Indicator._onChange = onChange;
onChange(); //Is that the way to call it?
expect(refresh).toBeCalled(); //Fails
expect(setInterval.mock.calls.length).toBe(1); //Fails
// I am trying to execute the 1 second timer till finishes (would be 60 seconds)
jest.runAllTimers();
expect(Indicator.state.elapsed).toBe(0); //Fails (I know is wrong but this is the idea)
expect(clearInterval.mock.calls.length).toBe(1); //Fails (should call this function when time elapsed is 0)
});
});
I am still misunderstanding something...
It looks like you're on the right track. Just to make sure everyone's on the same page for this answer, let's get some terminology out of the way.
Mock: A function with behavior controlled by the unit test. You usually swap out real functions on some object with a mock function to ensure that the mock function is correctly called. Jest provides mocks for every function on a module automatically unless you call jest.dontMock
on that module's name.
Component Class: This is the thing returned by React.createClass
. You use it to create component instances (it's more complicated than that, but this suffices for our purposes).
Component Instance: An actual rendered instance of a component class. This is what you'd get after calling TestUtils.renderIntoDocument
or many of the other TestUtils
functions.
In your updated example from your question, you're generating mocks and attaching them to the component class instead of an instance of the component. In addition, you only want to mock out functions that you want to monitor or otherwise change; for example, you mock _onChange
, but you don't really want to, because you want it to behave normally—it's only refresh
that you want to mock.
Here is a proposed set of tests I wrote for this component; comments are inline, so post a comment if you have any questions. The full, working source for this example and test suite is at https://github.com/BinaryMuse/so-jest-react-mock-example/tree/master; you should be able to clone it and run it with no problems. Note that I had to make some minor guesses and changes to the component as not all the referenced modules were in your original question.
/** @jsx React.DOM */
jest.dontMock('../indicator');
// any other modules `../indicator` uses that shouldn't
// be mocked should also be passed to `jest.dontMock`
var React, IndicatorComponent, Indicator, TestUtils;
describe('Indicator', function() {
beforeEach(function() {
React = require('react/addons');
TestUtils = React.addons.TestUtils;
// Notice this is the Indicator *class*...
IndicatorComponent = require('../indicator.js');
// ...and this is an Indicator *instance* (rendered into the DOM).
Indicator = TestUtils.renderIntoDocument(<IndicatorComponent />);
// Jest will mock the functions on this module automatically for us.
TriggerAnAction = require('../action');
});
it('waits 1 second foreach tick', function() {
// Replace the `refresh` method on our component instance
// with a mock that we can use to make sure it was called.
// The mock function will not actually do anything by default.
Indicator.refresh = jest.genMockFunction();
// Manually call the real `_onChange`, which is supposed to set some
// state and start the interval for `refresh` on a 1000ms interval.
Indicator._onChange();
expect(Indicator.state.elapsed).toBe(30);
expect(setInterval.mock.calls.length).toBe(1);
expect(setInterval.mock.calls[0][1]).toBe(1000);
// Now we make sure `refresh` hasn't been called yet.
expect(Indicator.refresh).not.toBeCalled();
// However, we do expect it to be called on the next interval tick.
jest.runOnlyPendingTimers();
expect(Indicator.refresh).toBeCalled();
});
it('decrements elapsed by one each time refresh is called', function() {
// We've already determined that `refresh` gets called correctly; now
// let's make sure it does the right thing.
Indicator._onChange();
expect(Indicator.state.elapsed).toBe(30);
Indicator.refresh();
expect(Indicator.state.elapsed).toBe(29);
Indicator.refresh();
expect(Indicator.state.elapsed).toBe(28);
});
it('calls TriggerAnAction when elapsed reaches zero', function() {
Indicator.setState({elapsed: 1});
Indicator.refresh();
// We can use `toBeCalled` here because Jest automatically mocks any
// modules you don't call `dontMock` on.
expect(TriggerAnAction).toBeCalled();
});
});