Hi there,
Apologies up front if this has been addressed before! My question is primarily about testing async lifecycle methods and handlers in React components. For example, I have a LoginForm
component with a click handler on the button that submits the form like this:
handleSignIn = async (event) => {
event.preventDefault();
try {
await this.props.tokenStore.setAccessToken(this.loginFormData);
} catch (error) {
console.error(error.message);
}
};
Pretty straightforward - it's just trying to grab an access token from our API using the user's data from the login form. The async nature of this handler is what's throwing me off in testing. I have a test using Jest and Enzyme like this:
it('should simulate signing in when valid user credentials are provided', (done) => {
const tokenStore = new TokenStore(apiMock);
const preventDefault = jest.fn();
const wrapper = mount(
<LoginForm
location={{ pathname: '/login' }}
tokenStore={tokenStore}
/>,
);
wrapper.find('input#username').simulate('change', {
target: {
name: 'username',
value: FAKE_USERNAME,
},
});
wrapper.find('input#password').simulate('change', {
target: {
name: 'password',
value: FAKE_PASSWORD,
},
});
wrapper.find('button').simulate('click', { preventDefault });
setTimeout(() => {
expect(preventDefault.mock.calls.length).toBe(1);
expect(tokenStore.accessToken).toBe(FAKE_ACCESS_TOKEN);
expect(wrapper.find('Redirect').length).toBe(1);
done();
}, 0);
Definitely not the best-written test ever, but I'm mostly just trying to get a feel for the different capabilities of Jest and Enzyme, and it illustrates what I'm trying to do well enough! I mount the component, simulate changing the username and password fields to valid mock values, simulate clicking the submit button, which calls the click handler from above and sets the access token (in the TokenStore
)...then I'm kind of lost on where to go next.
I've set jest.useRealTimers()
and use the setTimeout
and done
functionality as shown in the test, which works for expects
that pass. But if an expect
fails, I don't get the normal nice output, I just get Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL
, which makes me feel like I'm doing something funky here.
Any recommendations on what I could improve or do differently would be very welcome. Sorry if I'm missing something obvious!
I have the same issue. try/catch works fine but looks like it shouldn鈥檛 be necessary:
setTimeout(() => {
try {
expect(dispatch.mock.calls.length).toBe(2);
expect(dispatch.mock.calls[0]).toEqual([firstAction]);
expect(dispatch.mock.calls[1]).toEqual([secondAction]);
done();
}
catch (e) {
done.fail(e);
}
});
@cpojer Could you please take a look? Probably we鈥檙e doing something wrong here.
Ah, good call on the try/catch
; that does solve the issue I was having with the output! Although I'm still hoping there's a better way to accomplish all this, as @sapegin eluded to.
@pdhoopr would you mind providing a minimal repro of the issue?
Sure, @thymikee. Will this work?
https://github.com/pdhoopr/testing-async-react-methods-with-jest
Let me know if not. Thanks!
Thanks! I'll get to it as soon as I can
I have the same problem - failures inside a setTimeout function don't get printed, and the test takes the full jasmine.DEFAULT_TIMEOUT_INTERVAL
time to timeout, too. Is there a better way to test async code when it doesn't expose a promise?
Thanks for the workaround @sapegin, that'll help a lot in the meantime. I turned in into a little wrapper function:
function afterPromises(done, fn) {
setTimeout(function () {
try {
fn();
done();
} catch(e) {
done.fail(e);
}
}, 0);
}
Use like:
afterPromises(done, () => {
expect(false).toBe(true);
});
Relevant conversation on twitter, we have similar problem and use fix similar to above:
@dceddia Would it be beneficial to roll that type of logic into Jest?
@ConAntonakos Absolutely :) It'd be great if Jest automatically handled async calls like this.
Could we try and do something with async/await, I've wrapped it like this:
const itAsync = (description, func) => {
it(description, async (done) => {
try {
await func();
done();
} catch (err) {
done.fail(err);
}
});
};
Then it could be called like this
itAsync("Your test description", async () => {
// your async code
}
This should catch and run like normal. But the only problem being is that you would have to implement async/await in babel (or whatever you use).
In mocha, the done callback takes an optional error (standard Node.js cb style).
If you call done with a truthy value then it will be used as an error.
promise.then(x => {
// do something
done(); // successful
}).catch(err => {
// do something with error
done(err); // hand error to test runner
});
UPDATE: Jest now supports (as of v20.. but maybe earlier) passing an async function into it
i.e.
it('can render a product and initialize it', async () => {
await somethingAsync()
expect(true).toBe(true)
})
This issue is not really actionable from Jest's side. We may need to build some utilities around the React ecosystem, but it doesn't belong into Jest.
Most helpful comment
I have the same issue. try/catch works fine but looks like it shouldn鈥檛 be necessary: