“”
React Testing Library

Progressive Web Apps and Service Workers

View more techs

React Testing Library

 

Darío Macchi

Facundo Petre

May 6, 2021


Software testing is all about gaining confidence: When we're doing changes in our application, we don't want things to break.

So, what can we do to prevent production failures?

Well, we run our app and go throw the critical paths trying to find any new bug introduced by our changes. This could be really tedious work, but here is where automated testing could save us some time.


Today we are focusing on unit testing. We'll be testing separate chunks of code and we’ll do it keeping our test as close to the way a final user would use our app as possible.

Some tips:

- Don't focus on the implementation details of what you're testing.
- Try to keep the automated testing as close as possible to the way the user will interact with our components.
- Think how you would test your app if you were a manual tester.

Ok let's go:

To get this we are going to use React testing library (it comes ready to go with your create-react-app)

React testing library provides us a `render` function that allows us to render a component. It’s super straightforward, you just need to call the `render` function with the component you’d like to test, and that’s it.

We are also importing `screen`, which helps us to validate that the UI looks the way it should.

import { render, screen } from "@testing-library/react";
import App from "../App";

test('Render a component', ()=>{
render()

screen.debug()
})


It's important to render the component before starting to use screen methods. With screen.debug we get the React Node of the element printed on our terminal, so we can see what’s being rendered by our test. This is super useful and time-saving.

Our first validation:

With our component rendered, we are ready to start our UI-based validations. Our sample app has a counter, and it must start at 0 when it’s rendered, so let’s make sure this is happening.

You can learn more about screen getters here:
https://testing-library.com/docs/queries/about/#screen

In this scenario we are using screen.getByText, which takes a string or regex to get the element we want.

import { render, screen } from "@testing-library/react";
import App from "../App";

test('Render a component', ()=>{
  render()
  //  screen.debug() `screen.debug()` is for development purposes only so you should remove it when you are done.
  const counter = screen.getByText(/counter/i); // it expects a string or Regex to get the element.
  /*
    Here we go with our first UI assert, let's make sure the counter starts at `0`
    .toHaveTextContent along with many other useful assertions comes from `jest-dom`
    Have a look on all jest-dom assertions on it's: https://github.com/testing-library/jest-dom#table-of-contents
  */
  expect(counter).toHaveTextContent("Counter: 0");
})

`expect` is the way we assert the results of a test, it comes from the Jest testing framework (https://jestjs.io).

`toHaveTextContent` is part of jest-dom matchers, learn more:
https://github.com/testing-library/jest-dom

With those three lines, we just validated the initial state of our counter.

Now, let’s make sure it’s value increases and decreases when increase and decrease buttons are clicked.

Interacting with our components in the way a user would:

Here is where things get interesting: We’ll be using userEvent to simulate user interactions, such as clicks and keyboard strokes. We’ll be doing a basic usage of this but you can dive into its docs to check the full capabilities of userEvent:
https://github.com/testing-library/user-event

Let’s test the increment and decrement buttons.

The first thing we need to do is to use screen functions to get the `count` element, then we’ll need the `increment` and `decrement` buttons.

In this scenario we are using `screen.getByRole` (full description here), which takes the role of the element as first parameter and also takes an object as second parameter. We can use the name property of that object to query the elements of the given role.

Once we get the element, we’re using the `userEvent.click` function to click our elements, `userEvent.click` takes the element we’d like to click as its first parameter.

 import { render, screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import App from "../App";

 test('Test App counter', () =>{
   render()
   //  Second, we query our elements using `screen` getters
   const counter = screen.getByText(/counter/i);
   const increment = screen.getByRole("button", { name: /increment/i });
   const decrement = screen.getByRole("button", { name: /decrement/i });
   // We fire the click
   userEvent.click(increment);
   // Finally, we verify the results on the UI/
   expect(counter).toHaveTextContent("Counter: 1");
   // Same for decrement button
   userEvent.click(decrement);
   expect(counter).toHaveTextContent("Counter: 0");
 })


In the above test, we tested the functionality of the counter, without minding how the counter is implemented- we don’t care, the same way the final user won’t. This way we get a future-proof test decoupled from the implementation details.

Jest mock functions:

Next, we're using userEvent to fill some inputs on our Form component, but first, we’ll need to give a simple intro into Jest mock functions.

Basically, it is a function that allows us to expect a bunch of different things like the parameters the function was called with and the number of times the function was called.

Read more on the Jest docs. The syntax to declare a Jest mock function is:

 const mockFunction = jest.fn()

Testing our forms:

First we define our test data (`randomUser`), then we define a submit handler Jest mock function, and we pass it to Form’s props (just as we do when using the Form component). For this test we’re using `screen.getByLabel` to get our inputs, and `screen.getByRole` to get the send button.

To fill our inputs, we have userEvent.type, which takes as its first parameter the element we’d like to type on and the string we’d like to type as the second one.

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Form } from "../App";

test("Simulate keyboard typing with `userEvent`", () => {
  const randomUser = {
    user: "fpetre@vairix.com",
    password: ":party-parrot:",
  };
  const handleSubmit = jest.fn()
  render(<Form handleSubmit={handleSubmit} />);
  const userInput = screen.getByLabelText(/user/i);
  const passwordInput = screen.getByLabelText(/password/i);
  const sendBtn = screen.getByRole("button", { name: /send/i });
  // Now we'll use `userEvent.type` to fill the above inputs
  userEvent.type(userInput, randomUser.user);
  userEvent.type(passwordInput, randomUser.password);
  userEvent.click(sendBtn);
  expect(handleSubmit).toHaveBeenCalledWith(randomUser);
  expect(handleSubmit).toHaveBeenCalledTimes(1);
});

To sum up, we just filled the inputs, we hit the send button and we checked that the handleSubmit function was called with the right data. We also checked that the function was called once.

How can we deal with provider-dependent components? (Redux, ContextAPI).

Sometimes our components need a wrapper that provides its context. Gracefully, `render` function takes an options object as its second parameter and as you may be guessing: Yes, you can pass a wrapper as a render option.

First, let’s create a `test-utils.js` file:

// test-utils.js
import React from "react";
// we renamed render as rtlRender 
import { render as rtlRender } from "@testing-library/react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import userEvent from "@testing-library/user-event";
import reducer from "./redux/reducer";

function reduxRender(
  ui,
  // Options comes as second parameter
  { 
    initialState,
    store = createStore(reducer, initialState),
    ...renderOptions
  } = {}
) {
  // we create our wrapper function with the provider we need for our component
  const wrapper = ({ children }) => (
    {children}
  );
   // and we return the original `render` function with our wrapper and its options
  return rtlRender(ui, { wrapper, ...renderOptions });
}

// re-export everything
export * from "@testing-library/react";
export { userEvent };
export { reduxRender };
});

We just created our `reduxRender` function, and it’s ready to be used. But first, let’s do the same for a React ContextAPI example:

// test-utils.js
import React from "react";
import { render as rtlRender } from "@testing-library/react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import userEvent from "@testing-library/user-event";
import reducer from "./redux/reducer";
import ContextApiCounterProvider from "./context-api/ContextApiCounterProvider";
function reduxRender(
  ui,
  {
    initialState,
    store = createStore(reducer, initialState),
    ...renderOptions
  } = {}
) {
  const wrapper = ({ children }) => (
    {children}
  );

  return rtlRender(ui, { wrapper, ...renderOptions });
}

function contextRender(ui, { initialState }) {
   const wrapper = ({ children }) => (
     
       {children}
     
   );
 
   return rtlRender(ui, { wrapper });
 }

// re-export everything
export * from "@testing-library/react";
export { userEvent };
export { reduxRender, contextRender };

Just to show that the tests are actually the same, no matter if it's the Redux one or the ContextAPI one, we create our validateCounter function. We just need to call the respective render for each component, and call `validateCounter` with its initial value:

import React from "react";
import { reduxRender, contextRender, screen, userEvent } from "../test-utils";
import ReduxCounter from "../redux/Redux-counter";
import ContextApiCounter from "../context-api/ContextApiCounter";

const validateCounter = (initialCount) => {
  const counter = screen.getByText(/counter/i);
  const increment = screen.getByRole("button", { name: /increment/i });
  const decrement = screen.getByRole("button", { name: /decrement/i });
  expect(counter).toHaveTextContent(`Counter: ${initialCount}`);
  userEvent.click(increment);
  expect(counter).toHaveTextContent(`Counter: ${initialCount + 1}`);
  userEvent.click(decrement);
  expect(counter).toHaveTextContent(`Counter: ${initialCount}`);
};

describe("Dealing with providers", () => {
  test("Testing our ReduxCounter", () => {
    const initialState = 5;
    reduxRender(, { initialState });
    validateCounter(initialState);
  });

  test("Testing our ContextApiCounter", () => {
    const initialState = 10;
    contextRender(, { initialState });
    validateCounter(initialState);
  });
});


What if we need to interact with APIs?

Here is where MSW joins the party. Sometimes we need to test how our components integrate with external APIS. Luckily we have MSW; a full set of API mocking tools that intersects requests on the network level. We'll be covering just a few of them, specifically `rest` and `setupServer`. You can have a look on MSW: https://mswjs.io/

First we need to set up our mocking server:

import { render, screen, waitForElementToBeRemoved, } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
import App from "../App";

// here we go with the serverSetup
const server = setupServer();
const rickAndMortyApi = `https://rickandmortyapi.com/api/character/1`;
/*
The following functions calls are Jest utilities you can on its names to know what they do.
*/ 


beforeAll(() => {
  server.listen();
});
afterEach(() => {
  /*
    When mocking http request, 
    it's important to clear previous handlers when you jump to next
    test to prevent conflicts with handlers
  */
   server.resetHandlers();
});

afterAll(() => {
  server.close();
});


Alright, we are done with the server setup, now let’s write some tests.

Happy path:

test("Mock successful request", async () => {
  const mockResponse = {
    name: "Facundo",
    species: "human",
    status: "low-battery",
  };
  // `server.use` along with `rest` gives us the chance to mock the implementation of an api
  // we just need to pass our rest handlers and we can mock the endpoint implementation
  server.use(
    rest.get(rickAndMortyApi, (req, res, ctx) => {
      // ctx provide us with some useful methods
      // https://mswjs.io/docs/api/context
      // this is really expressJs like syntax BUT return is required (yup I had a good time debugging this)
      return res(ctx.json(mockResponse));
    })
  );
  render();
  userEvent.click(screen.getByRole("button", { name: /get rick/i }));
  // waitForElementToBeRemoved well it does what it says
  await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
  expect(screen.getByText(/name/i)).toHaveTextContent(mockResponse.name);
  expect(screen.getByText(/status/i)).toHaveTextContent(mockResponse.status);
  expect(screen.getByText(/species/i)).toHaveTextContent(
    mockResponse.species
  );
});

Unhappy path:

test(`Mock request failure`, async () => {
  const message = `R.I.P.`;
  server.use(
    rest.get(rickAndMortyApi, (req, res, ctx) => {
      // It's pretty same as before but setting an error status code
      // Delay here is completely optional, just to show that you can execute as many ctx methods as needed
      return res(ctx.delay(1000), ctx.status(500), ctx.json({ message }));
    })
  );
  render();
  userEvent.click(screen.getByRole("button", { name: /get rick/i }));
  // waitForElementToBeRemoved second parameter is an object with options: https://testing-library.com/docs/dom-testing-library/api-async/#waitforelementtoberemoved
  await waitForElementToBeRemoved(() => screen.getByText(/loading/i), {
  timeout: 2000,
  });
  expect(screen.getByRole("alert")).toHaveTextContent(message);
});


Thanks for reading!

That’s all for now: we covered some simple scenarios, the main idea of this blog is to show how simple it could be to start doing some automated testing with this powerful tool. I strongly recommend going through the docs to have a look at the full capabilities that React Testing Library provides.

Also, I recommend having a look at Kent’s blog for more useful information on how to improve our testing, JavaScript and React skills: https://kentcdodds.com/blog/

Last but not least, here is a link to my sample repo for the tests we did. Please feel free to submit an issue on anything you think could be improved (or maybe a PR if you’d like to) https://github.com/facundop3/testing-react-workshop

 

Dario Macchi

Facundo Petre

Facundo is a senior frontend developer currently focused on the ReactJS and Express combo. With over three years of experience working for the US market, Facundo is a major tech enthusiast who enjoys keeping the team and himself up to date with the latest trends.

Contact us

Ready to get started? Use the form below or give us a call to meet our team and discuss your project and business goals.
We can’t wait to meet you!


Follow Us
See our client reviews