73

Code

import { createUser } from '../services';
...
...

handleFormSubmit = () => {  
  this.setState({ loading: true });
  createUser()
    .then(() => {
      this.setState({
        loading: false,
      });
    })
    .catch(e => {
       this.setState({
         error: e,
       });
    });
};

Test

 it('rejects...', () => {
    const Container = createUserContainer(CreateUser);
    const wrapper = shallow(<Container />);  

    return wrapper.instance().handleFormSubmit()
      .catch(e => {
         console.log("State: ", wrapper.state());
         expect(e).toEqual('error');
      });
 });

Mock

export const createUser = function() {
  return new Promise((resolve, reject) => {
    reject('error');
  });
};

The test does force the code to go into the catch in the method. So the state does get set to 'error'.

But in my test, it doesn't do what I expect and wait for the Promise to reject before it tests for the state change. I'm not sure what to try here, should I be using async/await?

So it's the createUser method I want to wait for but I'm not sure my implementation allows for this.

Technotronic
  • 8,424
  • 4
  • 40
  • 53
coding123
  • 823
  • 1
  • 8
  • 12

4 Answers4

69

You should do something like this:

it('rejects...', () => {
  const Container = createUserContainer(CreateUser);
  const wrapper = shallow(<Container />);  
  return expect(wrapper.instance().handleFormSubmit()).rejects.toEqual('error');
});

I think it is cleaner this way. You can see this approach in the official docs.

It's important to note that .rejects (and .resolves) returns a promise, which is returned in the example above so that jest knows to wait on it. If you don't return it, you MUST await it:

it('rejects...', async () => {
  const Container = createUserContainer(CreateUser);
  const wrapper = shallow(<Container />); 
  await expect(wrapper.instance().handleFormSubmit()).rejects.toEqual('error');
});
Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96
Andres Gardiol
  • 1,312
  • 15
  • 22
  • 7
    This is the official way to check async errors. This should be the accepted answer. – Technotronic Feb 24 '21 at 09:21
  • 2
    Note: should probably make the method async and add await before expect (see linked official docs) – Ahmed-Anas Sep 30 '21 at 06:51
  • 1
    @Ahmed-Anas see the first example in the link. The one that starts with `t('tests error with rejects', () => {` not the other. – Andres Gardiol Sep 30 '21 at 12:54
  • I had to make make a slight variation to make this work: `expect(wrapper.instance().handleFormSubmit()).rejects.toEqual(Error('error));` – Jordi Oct 18 '21 at 12:53
  • @Jordi that depends on what your tested method returns – Andres Gardiol Jan 14 '22 at 12:54
  • I've had success with using the syntax: ```expect(wrapper.instance().handleFormSubmit()).rejects.toThrow('error text')``` – Dave Amphlett May 11 '22 at 12:10
  • Worth noting that if you `await` code like `wrapper.instance().handleFormSubmit()`, a rejected promise will throw, so make sure you don't `await` when using `expect...rejects` – killthrush Jan 17 '23 at 15:16
22

The test fails because it's not aware that the subject is asynchronous. It can be fixed by using a done param or making the test function async.

Note it's also necessary to set the number of expected assertions so that the test will fail even if the catch branch is not taken.

async/await style:

 it('rejects...', async () => {
    expect.assertions(1);
    const Container = createUserContainer(CreateUser);
    const wrapper = shallow(<Container />); 

    await wrapper.instance().handleFormSubmit()
      .catch(e => {
         console.log("State: ", wrapper.state());
         expect(e).toEqual('error');
      });
 });

Older style done param:

 it('rejects...', done => {
    expect.assertions(1);
    const Container = createUserContainer(CreateUser);
    const wrapper = shallow(<Container />);  

    wrapper.instance().handleFormSubmit()
      .catch(e => {
         console.log("State: ", wrapper.state());
         expect(e).toEqual('error');
         done();
      });
 });

Asynchronous Testing Reference

expect.assertions reference

Casey Watson
  • 51,574
  • 10
  • 32
  • 30
  • 7
    wouldn't this test give false positive if the function didn't throw? – oyalhi Feb 14 '20 at 21:09
  • 6
    @oyalhi no, it won't give a false positive. The line `expect.assertions(1)` tells jest that there will be an assertion so if the `catch` isn't triggered, jest will complain about the missing assertion – A F Feb 23 '20 at 00:31
4

Your code looks correct. Why do you say that it doesn't wait for the Promise to reject? The only difference I would make would be to make use of Jest's mocking capability, so change

Mock

export const createUser = function() {
  return new Promise((resolve, reject) => {
    reject('error');
  });
};

to

Test

jest.mock('../services');
const services = require('../services');

const createUser = jest.spyOn(services, "createUser");
createUser.mockRejectedValue("error");

...

it('rejects...', () => {

There's no need to have a separate Mock file

Community
  • 1
  • 1
Shiraz
  • 2,310
  • 24
  • 26
  • Note that your test can also confirm that state.loading is still set to true – Shiraz Jul 20 '19 at 11:30
  • What if the target code of the test needed to use "new services" thus you needed to do jest.doMock with a requireActual and override just a single function? – Brian Sterling Jul 03 '20 at 15:52
0

In your code handleFormSubmit function should return Promise on which you can wait in your test. Also you need to return truthful data from success and error callback to resolve and reject the promise respectively.

handleFormSubmit = () => {  
  this.setState({ loading: true });
  return createUser()
    .then(() => {
      this.setState({
        loading: false,
      });
      return true;
    })
    .catch(e => {
       this.setState({
         error: e,
       });
       throw e;
    });
};

Here in your actual code you have caught the error in catch handler and trying to catch it further in out test case code. Hence catch can not be chained further, while you can chain then multiple times.

For reference go through Promise documentations: https://www.peterbe.com/plog/chainable-catches-in-a-promise

Hriday Modi
  • 2,001
  • 15
  • 22
  • 3
    this didn't solve the issue. The code still goes into the catch, but the test still does not wait for the promise to fulfill before running the expects – coding123 Jun 13 '18 at 09:53
  • My bad, Have updated the answer with documentation link. It will surely solve your problem – Hriday Modi Jun 13 '18 at 17:39