Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to test component with async componentDidMount #1581

Closed
2 of 10 tasks
spplante opened this issue Mar 15, 2018 · 28 comments
Closed
2 of 10 tasks

How to test component with async componentDidMount #1581

spplante opened this issue Mar 15, 2018 · 28 comments

Comments

@spplante
Copy link

spplante commented Mar 15, 2018

Current behavior

I have a really typical dummy component which :

  • Initially renders a progress icon
  • Once the componentDidMount, executes an async callback from its properties to get it's data
  • Once the data came back from the callback's promise, updates the state
  • Re-renders with the data and without the progress

Typically, these are the relevant parts of the dummy component :

constructor(props: IMyComponentProperties, state: IMyComponentState) {
    super(props);
    this.state = { loading: true, data: null, error: null };
}
public componentDidMount() {
    this.props.loadData().then((result) => {
        this.setState({ loading: false, data: result, error: null });
    }) 
    .catch((error: any) => {
        this.setState({ loading: false, data: null, error: error });
    });
}
public render() {
    const loading = this.state.loading ? <Spinner label='Loading...' /> : <div />;
    const error = this.state.error != null ? <div>There was an error...</div> : <div />;

    return (
        <div>
            {loading}

	    { !this.state.loading && !this.state.error && 				
	         // Rendering this.state.data here...
            }

            {error}
        </div>
    );
}

Looking at the code above, the following test is executing just fine :

it('Should render a progress component while loading data', () => {
    shallowWrapper = shallow(<MyComponent loadData={mockedCallback} />);
    const containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
    expect(containsSpinner).to.be.true;
});

In fact, the shallow does render with a spinner because the promise within componentDidMount didn't resolve immediately, which is totally fine for this test.

Now If I want to test the opposite, which is to make sure the Spinner does NOT render after the data has been loaded, what would be the best way to do this? I guess I could listen to the componentDidUpdate and validate once it has been called, but how would I do this? Here is an awefull bit of code that currently works just to illustrate the problem :

it('Should NOT render a progress component when loading data is done', () => {
    shallowWrapper = shallow(<MyComponent loadData={mockedCallback} />);

    setTimeout(() => {
        shallowWrapper.update();
        let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
        expect(containsSpinner).to.be.false;
    }, 2000);
});

The test above works correctly, I just wait 2 seconds in order to wait for the promise to be completed, then I update the shallow and confirm that the progress isn't there anymore, but I do hope there is a cleaner way to do this as this is quite awful.

The pattern used by this dummy component really is typical, but somehow I just can't find any relevant documentation on how to test this kind of scenario :|

Would appreciate if someone could help 👍

Thanks!

API

  • shallow
  • mount
  • render

Version

library version
Enzyme 3.3.0
React 15

Adapter

  • enzyme-adapter-react-16
  • enzyme-adapter-react-15
  • enzyme-adapter-react-15.4
  • enzyme-adapter-react-14
  • enzyme-adapter-react-13
  • enzyme-adapter-react-helper
  • others ( )
@koba04
Copy link
Contributor

koba04 commented Mar 16, 2018

@spplante

What is the implementation of mockedCallback?
If the mockedCallback is something like this, you can call setTimeout without the ms of 2nd argument.

mockedCallback = () => Promise.resolve(data);

If mockedCallback calls an API, what about this?

let promise = Promise.resolve();
const loadData = () => {
  return promise.then(mockedCallback);
};
shallowWrapper = shallow(<MyComponent loadData={loadData} />);

promise.then(() => {
  shallowWrapper.update();
  let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
  expect(containsSpinner).to.be.false;
});

@spplante
Copy link
Author

spplante commented Mar 16, 2018

@koba04, my mockedCallback is indeed returning a Promise which resolves a mocked object. For your first solution, I managed to make it work with something similar using this :

it('Should NOT render a progress component when loading data is done', () => {
    shallowWrapper = shallow(<MyComponent loadData={mockedCallback} />);

    setImmediate(() => {
        shallowWrapper.update();
        let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
        expect(containsSpinner).to.be.false;
    });
});

From what I understand, the setImmediate guarantees that its callback will be called AFTER any previously executed promise, so in my case it does work.

For your second solution however, correct me if I am wrong but this won't work because even if the promise.then guarantees that the data has been returned back, nothing guarantees that the setState from within the component has been called, and nothing guarantees that the render() from the component is done either.

I tried something similar doing this :

it('Should NOT render a progress component when loading data is done', () => {
    let spy = sinon.spy(mockedCallback);
    shallowWrapper = shallow(<MyComponent loadData={spy} />);
    let spyCall = spy.getCall(0);

    spyCall.returnValue.then(() => {
        shallowWrapper.update();
        let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
        expect(containsSpinner).to.be.false;
    });
});

The code above does pretty much the same thing, it waits for the mockedCallback's promise to be completed before doing assertions, but it still fails because the setState/render from within the component isn't done yet which is understandable since we only waited for the promise, not necessarily for the render() to be called using the promise result.

So far the setImmediate seems to be the way to go, I was just wondering if there was anything better.

@koba04
Copy link
Contributor

koba04 commented Mar 16, 2018

@spplante Ah, you are right. My second example doesn't work.
This is a fixed version. You can try it.

let promise;
const loadData = () => {
  promise = Promise.resolve().then(mockedCallback);
  return promise;
};
shallowWrapper = shallow(<MyComponent loadData={loadData} />);

promise.then(() => {
  shallowWrapper.update();
  let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
  expect(containsSpinner).to.be.false;
});

@koba04
Copy link
Contributor

koba04 commented Mar 16, 2018

Anyway, this is not an issue of enzyme so stackoverflow might be a better place to ask the question.

@spplante
Copy link
Author

spplante commented Mar 16, 2018

@koba04, then again I don't see how this would work, because the only thing your promise.then guarantees at this point, is to land exactly there :

public componentDidMount() {
    this.props.loadData().then((result) => {
        // ---> Here you are after your promise.then(), the setState below hasn't necessarily been called and the render() neither
        this.setState({ loading: false, data: result, error: null });
    }) 
}

So making assertions on the newly rendered content doesn't work since you did wait for the new data but not necessarily for the new data to be rendered properly.

I ask the question here because it is pretty much the typical scenario of 90% of the React components and I can't find any reliable documentation on the best practices to test this typical scenario :

  • A component loads with no data
  • Gets the data async
  • Than rerenders with the updated data

There should be a straight forward way to test this :|

@koba04
Copy link
Contributor

koba04 commented Mar 16, 2018

@spplante Did you try my example?
The below test works fine for me.

const enzyme = require('enzyme');
const Adapter = require('enzyme-adapter-react-15');
const React = require('react');

enzyme.configure({adapter: new Adapter()});
const {shallow} = enzyme;

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loaded: false,
    };
  }
  componentDidMount() {
    this.props.loadData().then((result) => {
      this.setState({loaded: true, data: result});
    });
  }
  render() {
    return <div>{this.state.loaded ? this.state.data.foo : 'loading'}</div>
  }
}

describe('#1581', () => {
  it('should works fine', done => {
    const mockedCallback = () => Promise.resolve({foo: 'bar'});
    let promise;
    const loadData = () => {
      promise = Promise.resolve().then(mockedCallback);
      return promise;
    };

    const wrapper = shallow(<App loadData={loadData} />);
    expect(wrapper.text()).toEqual('loading');
    promise.then(() => {
      wrapper.update();
      expect(wrapper.text()).toEqual('bar');
      done();
    });
  });
});

The above test guarantee that the assertion is evaluated after calling mockedCallback, in this case, ShallowRenderer processes setState synchronously so the promise.then is processed after the setState and render. The behavior might be changed in the future version, but it works fine at the current version.

then again I don't see how this would work, because the only thing your promise.then guarantees at this point, is to land exactly there :

You can imagine the test flow like this.

public componentDidMount() {
    this.props.loadData().then((result) => {
        // Here you are after your promise.then(), the setState below hasn't necessarily been called and the render() neither
        this.setState({ loading: false, data: result, error: null });
    }) 
    .then(() => {
      wrapper.update();
      expect(wrapper.text()).toEqual('bar');
    });
}

@spplante
Copy link
Author

spplante commented Mar 16, 2018

@koba04 my mistake you are absolutely correct, it works. I also made it work using sinon to listen on mockedCallback which is pretty much the exact same thing as your example :

it('Should NOT render a progress component when loading data is done', () => {
    let spy = sinon.spy(mockedCallback);
    shallowWrapper = shallow(<MyComponent loadData={spy} />);
    let spyCall = spy.getCall(0);

    spyCall.returnValue.then(() => {
        shallowWrapper.update();
        let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
        expect(containsSpinner).to.be.false;
    });
});

I guess I am misunderstanding how the Promise works in this particular scenario, because I just don't get how waiting for the original promise (which is only responsible for returning data) ALSO waits for the results to be rendered synchronously, how is that possible?

  • All the original promise does is get the data asynchronously, it is not responsible for setting the state nor rendering anything.
  • When the component calls the original promise and gets the results in the .then(), to me this is where I would expect the original promise to be done, how are the next synchronous instructions (setState & render) included is a mystery for me haha

Edit : I totally get it the devil is in the details. The reason it works is because both .then() are stacked one after the other in the correct order on the same promise instance, the first .then being added by the component and the second .then being added by the unit test, thus the reason why the .then from the unit test is called at last.

Thanks a lot for this, the issue can be closed 👍

@koba04
Copy link
Contributor

koba04 commented Mar 17, 2018

Thank you! Please close the issue. (I can't close this because I don't have the permission 😅)

@shivasai09
Copy link

it('Should NOT render a progress component when loading data is done', () => {
    let spy = sinon.spy(mockedCallback);
    shallowWrapper = shallow(<MyComponent loadData={spy} />);
    let spyCall = spy.getCall(0);

    spyCall.returnValue.then(() => {
        shallowWrapper.update();
        let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
        expect(containsSpinner).to.be.false;
    });
});

@spplante
from your simplified solution ,where you used sinon.spy(mockedCallback).
your code is confusing me , because you have used the shallow() API so there is no chance of execution of the code inside the componentDidMount then how come did your spy function returned the value if it was never called..
secondly i have a doubt dose componentDidMount will also get called when you use shallow API

please reply me
@spplante @koba04 @ljharb

@spplante
Copy link
Author

spplante commented May 3, 2018

@shivasai09 as of enzyme v3, the LifeCycleExperimental switch that we had to specify to the shallow constructor in order to run the lifecycle methods is now enabled by default and stable.

See these posts to see what I am talking about :

https://stackoverflow.com/questions/47309585/is-componentdidmount-supposed-to-run-with-shallow-rendering-in-enzyme

#1494

So yes, the componentDidMount does get called using the shallow. 👍

@shivasai09
Copy link

@spplante i have tried using your method , but the problem is when i write wrapper.update() for any test case, lets say test case-3, and in that test case3 i am syping one function lets say func1()
and then when i am trying to spy the same func1() in the immediate test case-4, it is throwing the error that you cannot wrap the already wrapped function.. This is happening only when i say wrapper.update() in the test case 3, other wise everything is working fine..

though i have written

spy.restore()
spy.resetHistory()

in test case 3 

that error is not handled how to tackle with this error
please reply @spplante

@spplante
Copy link
Author

spplante commented May 4, 2018

Not sure I understand but on my side I initialize the spy in every single different test so there is no way I can have this error...

@shivasai09
Copy link

shivasai09 commented May 4, 2018

@spplante i am also initializing the spy in every single different test..
like this --> let spy = sinon.spy(component.prototype,'funtion');
and at the end

spy.restore()
spy.resetHistory()

but the problem is only occurring when i am using
wrapper.update

do one thing duplicate the this below test suite and run it , i mean to say write it two times and see if error occurs

1) it('Should NOT render a progress component when loading data is done', () => {
    let spy = sinon.spy(mockedCallback);
    shallowWrapper = shallow(<MyComponent loadData={spy} />);
    let spyCall = spy.getCall(0);

    spyCall.returnValue.then(() => {
        shallowWrapper.update();
        let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
        expect(containsSpinner).to.be.false;
    });
});

2)it('Should NOT render a progress component when loading data is done', () => {
    let spy = sinon.spy(mockedCallback);
    shallowWrapper = shallow(<MyComponent loadData={spy} />);
    let spyCall = spy.getCall(0);

    spyCall.returnValue.then(() => {
        shallowWrapper.update();
        let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
        expect(containsSpinner).to.be.false;
    });
});

and please do reply the what is the behaviour
@spplante

@spplante are you using karma framework?

@spplante
Copy link
Author

spplante commented May 4, 2018

I am using karma, I am not a sinon or enzyme expert though, you might want to open an issue with sinon directly since this code is from a while ago and I don't recall having any problem with it :|

@shivasai09
Copy link

@spplante i request you to do what i have mentioned .. and please check the behaviour, but make sure you have wrapper.update() in both of the test cases.
i will definitely open a issue, but before that i want to know what is happening exactly,
so i request you @spplante to do what i said please

@spplante
Copy link
Author

spplante commented May 4, 2018

I do not have any project running this code, I opened this issue to understand my error, I was able to make it work with sinon.spy just for theoretical reasons but I ended up using the following code which is more simple than using sinon.spy for my case :

it('Should NOT render a progress component when loading entity relations is done', (done) => {
        setImmediate(() => { 
            shallowWrapper.update();
            let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
            expect(containsSpinner).to.equal(false);
            done(); 
        }); 
    });

Using the setImmediate fixed my original problem where I had to wait for 2 seconds using a timer. Back then I didn't understand why setImmediate was working and that's why I did it the long way with sinon but I don't have this code anymore and I don't feel like replicating the whole thing as I am currently working for a client.

Please open an issue with sinon.

@shivasai09
Copy link

@spplante thanks for the explianation
one last doubt can you please explain me why this code will not work

let promise = Promise.resolve();
const loadData = () => {
  return promise.then(mockedCallback);
};
shallowWrapper = shallow(<MyComponent loadData={loadData} />);

promise.then(() => {
  shallowWrapper.update();
  let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
  expect(containsSpinner).to.be.false;
});

because i am not seeing any difference with the fix version of this code which is below

let promise;
const loadData = () => {
  promise = Promise.resolve().then(mockedCallback);
  return promise;
};
shallowWrapper = shallow(<MyComponent loadData={loadData} />);

promise.then(() => {
  shallowWrapper.update();
  let containsSpinner = shallowWrapper.containsMatchingElement(<Spinner />);
  expect(containsSpinner).to.be.false;
});

whats the difference why it dint worked? because @spplante form your explination .then() will get executed one after the other , so i think there is no difference between the two codes because in both of this .then() is in perfect order ..

then what's wrong in the first code?

@spplante waiting for you reply, this one doubt will save my day..
Thanks in advance

@spplante
Copy link
Author

spplante commented May 7, 2018

Hard to tell honestly I would expect both of the codes to work

@kairiruutel
Copy link

@shivasai09 .then() block will be executed right after the promise gets resolved. In first example the promise is being resolved in the first line and in the second example it gets resolved once loadData is executed.

@hansiemithun
Copy link

Sorry, i read the complete blog and with some guarantee that i might get an solution to my problem which is very similar to this and posted in: https://stackoverflow.com/questions/51550520/testing-promise-functions-in-jest

In my example, the props are bind with mapStateToProps from redux and not passing to component. So, please help me how to cover this scenario. I am new to React and Jest. Thanks in advance.

@AndrewRayCode
Copy link

AndrewRayCode commented Aug 23, 2018

I don't know if this will help anyone, but this pattern seems to work for me:

  componentWillMount() {
    return Promise.all([this.promise1(), this.promise2()]);
  }

and in your specs:

it('thing', async () => {
      const willMount = spy(MyComponent.prototype, 'componentWillMount');
      const wrapper = shallow(<MyComponent {...props} />);
      await willMount;
      wrapper.update().find(...)
});

I'm not sure why you need the wrapper.update since I would expect a re-render to trigger, but this works

edit or even nicer

const willMount = async (ComponentClass, props) => {
  const willMount = spy(CreateCaseForm.prototype, 'componentWillMount');
  const wrapper = shallow(<ComponentClass {...props} />);
  await willMount;
  return wrapper;
};
it('thing', async () => {
      const wrapper = await willMount(MyComponent, props);
      wrapper.update().find(...)
});

@ljharb
Copy link
Member

ljharb commented Aug 23, 2018

Posted this answer here:

@AndrewRayCode i'm not sure that works how you think; in that case, willMount is a spy, not a promise, so await willMount is just "wait til the next tick". However, you could use await willMount.returnValue (i think) and then it would do what you expect.

@theflyingmantis
Copy link

theflyingmantis commented Mar 19, 2019

If anyone is still over this, read these links: https://stackoverflow.com/a/53182054/5285338, jestjs/jest#2157 (comment), #346 (comment) (this one puts the first promise of the mount by sinon stub)
TBH: There is no official solution - just hacks. Using the race condition property

@gil-air-may
Copy link

I'm still trying to find a work around for this. None of the strategies I found in here worked for me. The well known flushPromises hack does not work for me since the http request on my componentDidMount never finishes in time and is not affected by the flushPromises call.

@ljharb
Copy link
Member

ljharb commented May 14, 2019

@gil-air-may your test needs a way to get at the http request promise, so it can await on it.

@adueck
Copy link

adueck commented Sep 24, 2019

@gil-air-may I have the exact same issue/use case. Any resolve on this? I need to wait for at http request triggered from componentDidMount

@krichter722
Copy link

Inspired by https://stackoverflow.com/questions/49419961/testing-with-reacts-jest-and-enzyme-when-async-componentdidmount I added await to almost everything I found and now the following is working (not the necessary async in the it argument):

it('works', async () => {
    fetchMock.get('https://example.org/rest', '[]');

    const wrapper = await mount(<Component />);

    await wrapper.instance().componentDidMount();
    await wrapper.update();
    expect(wrapper.html()).to.equal("<some><html/></some>");
});

@ljharb
Copy link
Member

ljharb commented Oct 10, 2019

@krichter722 adding await to things that aren't promises is at best redundant and at worst needlessly slows down your code. The only thing there that could possibly return a promise is wrapper.instance().componentDidMount();; everything else is just creating a race condition where you're getting lucky that your test passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests