DEV Community

charlie⚡
charlie⚡

Posted on • Edited on

Formik & React Testing Library and Screaming at a Computer for an Hour

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');
  });
});

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And typically in your CI process (like Travis or GitHub Actions), you should include:

jest --no-cache
Enter fullscreen mode Exit fullscreen mode

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);
});

Enter fullscreen mode Exit fullscreen mode

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');
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
maximecote57 profile image
Maxime Côté

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 :(.

Collapse
 
pighqs profile image
Peggy

Hi Maxime,
did you find a way to fix it ?

Collapse
 
rangeoshun profile image
Abel Varga

Yeah, this will give you a false positive.

Collapse
 
charliewilco profile image
charlie⚡

how so?

Collapse
 
rangeoshun profile image
Abel Varga

If you do not await the wait in an async function, the test is over before the expectation could be evaluated.

Collapse
 
rangeoshun profile image
Abel Varga

In my case, I needed not just await the expect, but the problem also was the Formik did not call the handleSubmit callback. I had to fire a submit event on the form element itself.

Thread Thread
 
xap5xap profile image
XAVIER PEREZ

Hello. How do you submit an event on the form in the test?

Thread Thread
 
rangeoshun profile image
Abel Varga • Edited

Basically like this:

fireEvent.submit(container.querySelector('form'))

container is the value returned in the object by the render method.

Collapse
 
dannypule profile image
Danny Pule

wait() was recently deprecated and replaced with waitFor() testing-library.com/docs/dom-testi...

Collapse
 
kellyrmilligan profile image
kellyrmilligan

Thanks! this worked like a charm for me.

Collapse
 
niubo profile image
Rolando Niubo

Amen

Collapse
 
vinyoliver profile image
Viny Machado

Thanks man!

Collapse
 
stuj1 profile image
Stuart Jennings • Edited

Thanks. This held me up for more than an hour. Never thought to wait on the assertions!