Test a React Component function with Jest
Asked Answered
O

2

32

Original

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?


Update 1, based on Ian answer

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...

Obligation answered 27/8, 2014 at 17:4 Comment(3)
I'm struggling with exactly the same problem at work right now. Thank you for taking the time to write a question and hopefully get an answer to itBarra
I believe toBeCalled is only valid on a mock, not an actual function, e.g. as returned by jest.genMockFunction(). See facebook.github.io/jest/docs/mock-functions.html#content ; presumably you'd need to replace Indicator.refresh with a mock implementation.Siloam
Hi Brandon. But what I want to test it's if my component calls that function when it has to call it. So, I am not sure how to use a mock function in this case.Obligation
S
46

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();
  });
});
Siloam answered 28/8, 2014 at 20:19 Comment(3)
This answer is totally amazing (same as the full example in your repo). Thank you! Now I understand much more how Jest works. Only one comment, I have my SetIntervalMixin in another file so for make it run, it is also necessary to call jest.dontMock('../SetIntervalMixin');Obligation
Thank you for this very thorough answer. I'm trying to emulate it on a component that requires a store and am having trouble. I am using `jest.dontMock('./Store') but it looks Jest is still trying to mock it. I get the message "Cannot call method 'register' of undefined" for the Store. Have you run into this scenario as well?Dariusdarjeeling
After making it through my initial Jest setup, I found myself led here by another search for another unrelated problem. This great answer helped me again! I wish I could upvote twice.Dariusdarjeeling
P
6

I think I understand what you're asking, at least part of it!

Starting with the error, the reason you are seeing that is because you have instructed jest to not mock the Indicator module so all the internals are as you have written them. If you want to test that particular function is called, I'd suggest you create a mock function and use that instead...

var React = require('react/addons');
var Indicator = require('../Indicator.js');
var TestUtils = React.addons.TestUtils;

var refresh = jest.genMockFunction();
Indicator.refresh = refresh; // this gives you a mock function to query

The next thing to note is you are actually re-assigning the Indicator variable in your example code so for proper behaviour I'd rename the second variable (like below)

var indicatorComp = TestUtils.renderIntoDocument(<Indicator />);

Finally, if you want to test something that changes over time, use the TestUtils features around timer manipulation (http://facebook.github.io/jest/docs/timer-mocks.html). In your case I think you can do:

jest.runAllTimers();

expect(refresh).toBeCalled();

Alternatively, and perhaps a little less fussy is to rely on the mock implementations of setTimeout and setInterval to reason about your component:

expect(setInterval.mock.calls.length).toBe(1);
expect(setInterval.mock.calls[0][1]).toBe(1000);

One other thing, for any of the above changes to work, I think you'll need to manually trigger the onChange method as your component will initially be working with a mocked version of your Store so no change events will occur. You'll also need to make sure that you've set jest to ignore the react modules otherwise they will be automatically mocked too.

Full proposed test

jest.dontMock('../Indicator');

describe('Indicator', function() {
  it('waits 1 second for each tick', function() {
    var React = require('react/addons');
    var TestUtils = React.addons.TestUtils;

    var Indicator = require('../Indicator.js');
    var refresh = jest.genMockFunction();
    Indicator.refresh = refresh;

    // trigger the store change event somehow

    expect(setInterval.mock.calls.length).toBe(1);
    expect(setInterval.mock.calls[0][1]).toBe(1000);

  });

});
Pick answered 28/8, 2014 at 5:55 Comment(2)
Hi Ian, thank you for taking the time on writing such an answer. Now I understand more concepts, however, I'm still not able to run my tests. I updated my question with the tests I am trying to run based on your answer. Still something is wrong.Obligation
I'm wondering what goes in the // trigger the store change event somehow section. Is this mocked somehow, or can you do something like Store.trigger('change')? I know that doesn't actually work, but just conceptually, how would you fire that trigger without sending an action to the dispatcher?Anagnos

© 2022 - 2024 — McMap. All rights reserved.