Testing Formik forms with react-testing-library

In this article we will walk through some basic examples of testing react form, built using formik library. We will be using react-testing-library for testing react components.

Quick Setup

Create a project using CRA

> npx create-react-app my-app

Add formik to our project.

> npm install formik - save

App Under Test

It's a simple react form, consisting of two components

  1. Email field: Built using Field component of formik. Is mandatory field. Used standard <Error> of formik to show errors
  2. Date Field: Built using react-datepicker. On date change we are calling helper function setFieldValue (provided by formik) to set value. Is mandatory field and same as above used <Error>to show errors
import React from "react";
import { Formik, Form, Field, ErrorMessage } from "formik";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
const App = () => (
<div>
<Formik
initialValues={{ email: "", date: "" }}
validate={values => {
let errors = {};
if (!values.date) {
errors.date = "Required";
}
if (!values.email) {
errors.email = "Required";
}
return errors;
}}
>
{({ isSubmitting, setFieldValue, values }) => (
<Form>
<label htmlFor="email">Email</label>
<Field id="email" type="email" name="email" />
<ErrorMessage data-testid="emailError" name="email" component="div" />
<br />
<DatePicker
selected={values.date}
onChange={event => {
setFieldValue("date", event);
}}
/>
<ErrorMessage data-testid="dateError" name="date" component="div" />
<br />
<button type="submit">Submit</button>
</Form>
)}
</Formik>
</div>
);
export default App;

Once the app is built we are good to start with testing it.

Install react-testing-library

CRA projects comes equipped with jest so we don't need to add any additional configuration for testing. But we need to install react-testing-library.

npm install --save-dev @testing-library/react

Things to Test

  1. Test validation of email field, simple formik component
  2. Test validation of date field, custom component

Test Case

Lets get started with first case

import React from "react";
import "@testing-library/jest-dom/extend-expect";
import { render, fireEvent, wait } from "@testing-library/react";
import App from "./App";
it("should show validation on blur", async () => {
const { getByLabelText, getByTestId } = render(<App />);
const input = getByLabelText("Email");
fireEvent.blur(input);
await wait(() => {
expect(getByTestId("emailError")).not.toBe(null);
expect(getByTestId("emailError")).toHaveTextContent("Required");
});
});

Let's see in detail what is happening here.

  1. We are using render function of react-testing-library to render
    . Render function returns lot of utility functions. In our case we are importing getByTestId& getByLabelText.
  2. Next we use getByLabelTextfunction to get email component from dom. For this function to work there should have for/htmlFor relationship between label and form element as highlighted below
<label htmlFor="email">Email</label>
<input id="email" />

If not found it raises exception and test fails.

  1. Once we get the input component, we call blur function on it using fireEvent utility function.
  2. Now we expect formik to run its magic and show validation errors. To verify this first we will get errorComponent using test id. And expect it not to be null.

But our test fails.

Formik validation is asynchronous function. And it takes little to time to display. And in our case we try to immediately find errorComponent after blur action and thus we are not able to find.

Solution

We use wait function of library to solve this problem. Basically will convert test function into async function so that we can use await method.

await wait(() => {
expect(getByTestId("emailError")).not.toBe(null);
expect(getByTestId("emailError")).toHaveTextContent("Required");
});

wait functions accepts a callback functions and waits till the expectation returns true. Once validation is added by formik our expectation will turn true and hence test passes.

Yee we were able to write our first test case in RTL.

Testing Custom component

Testing custom component is not as straight forward as testing standard field component of formik.  We can divide, change of date, scenario in 2 parts.

  1. Selection of date using react-datepicker component
  2. Setting of date in form

Writing test case which will mimics user behaviour, of changing date on browser is little complicated. And most importantly our intention here to test the validation part which is triggered after date is changed. So while testing this scenario we will ignore behaviour of react-datepicker. And just concentrate on code that we have written which is second part.

Mocking "react-datepicker"

By ignore I mean we will mock react-datepicker library in our test. So that when we render our form, calling<DatePicker /> will return <input /> from mocked function instead of actual calendar from library.

jest.mock("react-datepicker", () => props => (
<input
data-testid="mockedDateField"
onChange={e => {
props.onChange(e);
}}
/>
));

With this we can easily change date by just firing change event on above mocked field. Whereas in original function we need to perform several steps to set date.

Test case for custom component

test("should remove date error id we select date", async () => {
const { getByText, getByTestId, queryByTestId } = render(<App />);
const button = getByText("Submit");
fireEvent.click(button);
const mockedDateField = getByTestId("mockedDateField");
fireEvent.change(mockedDateField, { target: { value: new Date() } });
await wait(() => {
expect(queryByTestId("dateError")).toBe(null);
});
});

Lets see step by step details of above written test case

  1. Render <App> which has our form
  2. Find and click submit button. This will trigger validation and set error for all the fields
  3. Change date by firing change event on mocked date field. This will set date field in form. And thus removed error
  4. As validation in formik are asynchronous we need to use wait utility function. And expect the error on date component to be removed.

React apps can get complex. Mocking them as above makes it easier to write test cases. And most importantly we write test case for feature that we wrote, not for the imported components.

You can find the full code for this example at react-testing-library-formik-example