Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

I'm trying to mock out the a service that returns promises so that I can verify it gets called with the correct parameters. The way the service is called varies based on the state and the first call to the service sets the state .

When setting the state in the promise it is not updating unless I wrap the assertion in setTimeout or completely stub out the promise. Is there a way to do this with just a plain promise and an expect?

My component:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {results: []};
    this.service = props.service;
    this.load = this.load.bind(this);
  load() {
    if (this.state.results.length === 0) {
      this.service.load('state is empty')
                  .then(result => this.setState({results: result.data}));
    } else {
      this.service.load('state is nonempty')
                  .then(result => this.setState({results: result.data}));
  render() {
    return (
      <div className="App">
        <button id="submit" onClick={this.load}/>

My test:

it('Calls service differently based on results', () => {
  const mockLoad = jest.fn((text) => {
    return new Promise((resolve, reject) => {
      resolve({data: [1, 2]});
  const serviceStub = {load: mockLoad};
  let component = mount(<App service={serviceStub}/>);
  let button = component.find("#submit");
  button.simulate('click');
  expect(mockLoad).toBeCalledWith('state is empty');
  button.simulate('click');
  //this assertion fails as the state has not updated and is still 'state is empty'
  expect(mockLoad).toBeCalledWith('state is nonempty');

As mentioned, the following works, but I'd rather not wrap the expect if there's a way around it:

setTimeout(() => {
    expect(mockLoad).toBeCalledWith('state is nonempty');
    done();
  }, 50);

I can also change how I mock the function to stub out the promise which will work:

const mockLoad = jest.fn((text) => {
  return {
    then: function (callback) {
      return callback({
        data : [1, 2]

But I'd like to just return a promise.

React batches setState calls for performance reasons, so at this point

expect(mockLoad).toBeCalledWith('state is nonempty');

the condition

if (this.state.results.length === 0) {

is most likely still true, because data has not yet been added to state.

Your best bets here are

  • Either use forceUpdate between the first and second click event.

  • Or split the test into two separate, while extracting common logic outside of the test. Even the it clause will become more descriptive, for instance: it('calls service correctly when state is empty') for the first test, and similar for the second one.

  • I'd favour the second approach.

    setState() does not always immediately update the component. It may batch or defer the update until later.

    Read more here.

    Thanks! I tried both the suggestions but unfortunately neither of them worked, the problem seems to be with waiting for the promise to finish rather than the batching of setState as if I remove the promise and just call setState outside of a .then the test works fine. – Matt Fowler May 1, 2017 at 13:29 I see, it is then very unfortunate if the promise resolution is delayed until everything else is done... in this case I don't see an obvious clear solution either. Please post your solution if you find something elegant, thanks! – Lyubomir May 1, 2017 at 13:59

    Using Sinon with Sinon Stub Promise I was able to get this to work. The stub promise library removes the async aspects of the promise, which means that state gets updated in time for the render:

    const sinon = require('sinon');
    const sinonStubPromise = require('sinon-stub-promise');
    sinonStubPromise(sinon);
    it('Calls service differently based on results', () => {
        const mockLoad = jest.fn((text) => {
            return sinon.stub().returnsPromise().resolves({data: [1, 2]})();
        const serviceStub = {load: mockLoad};
        let component = mount(<App service={serviceStub}/>);
        let button = component.find("#submit");
        button.simulate('click');
        expect(mockLoad).toBeCalledWith('state is empty');
        button.simulate('click');
        expect(mockLoad).toBeCalledWith('state is nonempty');
    

    http://sinonjs.org/

    https://github.com/substantial/sinon-stub-promise

    Thanks for contributing an answer to Stack Overflow!

    • Please be sure to answer the question. Provide details and share your research!

    But avoid

    • Asking for help, clarification, or responding to other answers.
    • Making statements based on opinion; back them up with references or personal experience.

    To learn more, see our tips on writing great answers.