I have loved using both Formik and React Testing Library. At this point in the React community, I consider these tools to be nice sensible defaults of projects of any real size.
This afternoon, I needed to write some unit tests for some components that I had to add to my project and they were horribly broken.
import * as React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { Formik, Form, Field } from 'formik';
import { render, fireEvent, waitForElement } from '@testing-library/react';
describe('Very important form', () => {
it('submits values and fires', async () => {
const mock = jest.fn();
const { getByText, getByTestId } = render(
<Formik initialValues={{ name: '' }} onSubmit={mock}>
<Form>
<Field name="name" data-testid="Input" />
<button type="submit">Submit</button>
</Form>
</Formik>
);
const input = await waitForElement(() => getByTestId('Input'));
const button = await waitForElement(() => getByText('Submit'));
fireEvent.change(input, {
target: {
value: 'Charles',
},
});
fireEvent.click(button);
expect(mock).toBeCalled();
expect(mock.mock.calls[0][0].name).toBe('Charles');
});
});
What I want to validate is that mock
was called when the form submits and to see the results of onSubmit
include the value I typed.
Identifying the Problem
⛔️ Jest's Cache
Normally, when I have tests like this don't pass, where everything looks good, I start to blame Jest itself. Jest has a pretty heavy caching system. This allows you to continually be watching test files and run them very quickly and its cache is a good thing. But occasionally (am empirically) that cache gives false positives and I have found that clearing this cache and re-running your tests can give you the validation that your tests are rightfully passing. You can do that by running:
jest --clearCache
And typically in your CI process (like Travis or GitHub Actions), you should include:
jest --no-cache
But running your tests locally on your machine, caching is a good thing.
And with the cache cleared, still broken.
⛔️ Maybe act()
React DOM's test utils package (react-dom/test-utils
) has a utility called act()
and React Testing Library has a wrapper around it too. act()
(from what I understand) prepares a container to be updated by batching all updates like the way it would work in the browser. So things like updating the state or re-rendering components should be wrapped in act()
.
Anytime you're performing an async operation, it's helpful to wrap things in act()
and it's covered in the FAQ section of React Testing Library.
Wrapping the events that update the component like this:
import { act, fireEvent } from '@testing-library/react';
act(() => {
fireEvent.change(input, {
target: {
value: 'Charles',
},
});
});
act(() => {
fireEvent.click(button);
});
Didn't help, still broken.
⛔️ User Error
At this point, I read through (perhaps too quickly) both Formik and React Testing Library's documentation sites and not finding anything that stood out about the tests I was writing being wrong or missing anything.
I read through all of the documentation from Jest on using jest.fn()
mocks. And nothing. 😭
At this point, I was incredibly frustrated, I uttered every swear word at an array of volumes at my computer and even perhaps invented new swear words. I contemplated switching professions, I went for a walk around the office and drank a glass of water. 🤬
My tests were still broken. 😤
✅ A mysterious solution found buried in GitHub Issues
Then I searched for "React Testing Library" in the Issues section of the Formik repo and found this #1554. Since, Formik runs it's validations internally, async, then calls the onSubmit
props, we need to await the results. React Testing Library gives us a utility for this it's called wait()
. We need to wait to see if mock
is called and to checkout the results.
The solution looks like this:
import * as React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { Formik, Form, Field } from 'formik';
import { render, fireEvent, waitForElement, wait } from '@testing-library/react';
describe('Very important form', () => {
it('submits values and fires', async () => {
const mock = jest.fn();
const { getByText, getByTestId } = render(
<Formik initialValues={{ name: '' }} onSubmit={mock}>
<Form>
<Field name="name" data-testid="Input" />
<button type="submit">Submit</button>
</Form>
</Formik>
);
const input = await waitForElement(() => getByTestId('Input'));
const button = await waitForElement(() => getByText('Submit'));
fireEvent.change(input, {
target: {
value: 'Charles',
},
});
fireEvent.click(button);
wait(() => {
expect(mock).toBeCalled();
expect(mock.mock.calls[0][0].name).toBe('Charles');
});
});
});
And now my tests are passing.
Conclusion
Feel your feelings. If your tests aren't passing with either of these libraries, see if using wait()
or act()
might help and swearing at your computer isn't the worst thing in the world but having a glass of water is also a good idea.
Sign up for my newsletter, follow me on Twitter @charlespeters or find me at charlespeters.net.
Top comments (13)
Hi Charles ! I'm having kind of the same issue, but I'm not sure this really works. Since you don't await your
wait
call, I think your test will end before the expects in your wait argument succeed. I'm still trying to figure out how to fix this :(.Hi Maxime,
did you find a way to fix it ?
Yeah, this will give you a false positive.
how so?
If you do not await the wait in an async function, the test is over before the expectation could be evaluated.
In my case, I needed not just
await
theexpect
, but the problem also was the Formik did not call thehandleSubmit
callback. I had to fire asubmit
event on the form element itself.Hello. How do you submit an event on the form in the test?
Basically like this:
container
is the value returned in the object by therender
method.wait()
was recently deprecated and replaced withwaitFor()
testing-library.com/docs/dom-testi...Thanks! this worked like a charm for me.
Amen
Thanks man!
Thanks. This held me up for more than an hour. Never thought to wait on the assertions!