Testing with React Testing Library using component instances
Tags: testing, react, functional
Published: 2020-04-21 16:21:51

In the last year, I followed the trend in the industry, and I changed the way I test my React components from using [Enzyme](https://github.com/enzymejs/enzyme) to embrace [Testing Library](https://github.com/testing-library/react-testing-library).

The change has been for good! Now my tests are now more concise, clear and meaningful. They are especially easier to read because they don't have any code related to the component internals. And I love how one test can cover the interaction among many components, reaching very deep in the component tree.

But... sometimes I miss Enzyme.

## Why to test React component instances
When switching to Testing Library, we focus on the UI trying to avoid any contact with the internal implementation of our React components. Our tests become like final users, that know how to detect elements, click on them, type on the inputs... and our app should just work, no matter how things are handled in the background.

I have also discussed with my workmates about what's the difference with [Selenium](https://www.selenium.dev/) or [Cypress](https://www.cypress.io/) tests then? There should be any?

There are a lot of differences between the end to end tests we create with Selenium/Cypress to check our systems integration, and our deep React testing that should follow the specs without messing with servers or API interaction at all. But I see how there is some overlapping, and I understand people that get confused, especially if we see our React components only as UI.

**React components are more than UI**, they also contain logic, functionality that sometimes is much harder to test emulating a user clicking buttons, than with a good unit test. 

## Explosion of tests
Before showing an example of why sometimes it's nice to have access to the component instances in our testing, I'll share [react-dom-instance](https://github.com/arqex/react-dom-instance), a library to **find the component instances bound to DOM elements**. It plays really nicely with React Testing Library, without the need of re-installing Enzyme again.

When creating tests with Testing Library, there is an issue that I stumble upon often. It's about having my logic is in a parent component and a(many) stateful child component(s) that I need to interact with multiple times in order to check if the parent does what I expect.

Let's say we have this todo list app (credits to [Fumie Wada](https://codepen.io/hotate17)), rendered by the component `<TodoApp />`:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/5fi1r9sgzce6gv2lqb5c.png)

When we click on the "Create new item" link at the top right, a form is open to let us type a new todo item. The form is rendered by the `<CreateForm /> ` component:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0h2j57z3u8e6zcsh85z4.png)

When the user opens the form, type the text of the item, check if it's important, select a color and click on the "Add" button, our `TodoApp` component should:

* Create a new todo item with the text typed and the color selected.
* If it was marked as important, the item is also added to another list.
* Send an event to google analytics with the color selected, if it wasn't marked as important 🤷

In our `TodoApp.spec.js` file, we would have a test for this logic that would be something like:
```js
it('should create an important item', () => {
  const { queryByTestId } = render( <TodoApp /> );

  fireEvent.click( queryByTestId('openButton') );
  fireEvent.input( queryByTestId('todoInput'), {target: {value: "Buy some bread"}} );
  fireEvent.click( queryByTestId('color_red') );
  fireEvent.click( queryByTestId('importantCheckbox') );
  fireEvent.click( queryByTestId('addButton') );

  // We had mocked some actions beforehand
  expect( createItem ).toHaveBeenCalledWith( "id1", "Buy some bread", "red" );
  expect( addToImportant ).toHaveBeenCalledWith( "id1" );
  expect( trackGAEvent ).not.toHaveBeenCalled();
});
```

We are testing so mucho up there, and there are many things that could be done better, but just ignore them for the sake of the example. 

Let's focus on how the color that we clicked in a child component, it's the color that we are creating the todo with. We have many colors in the form, should we test all of them?

If we have not enabled the "Mark as important" option, should we check that we are tracking all the colors in Google Analytics properly again?

`TodoApp` component tests **shouldn't care about how many colors are, instead they should focus on completing the expected tasks once any color has been selected**. Not testing all the colors feels just bad, but all the clicking and typing we have to do for every test look very repetitive too.

The ability of React Testing Library to test nested components working together is amazing, but **it tends to move the checks to the top of the React tree**. When the children have many settings, we end up with really big test files in the top components, and those tests are usually made by repetitive tasks with small permutations of the checks we have to do. The more options we have in our child components, the bigger is the rate of growth for test cases in the parent component... it's like an explosion of tests.

## Splitting test files using instances
The `CreateForm` component has no complex logic, simply let the user type a name, select if it's important and pick a color. It doesn't know what to do with that information but, for example, it's responsible for how many colors are available to pick.

We can listen to the user's selection thanks to the `onAdd` prop, so a test file for `CreateForm` looks like the perfect place for testing all the colors that are available:

```js
it('should select the color red when clicking on the red input', () => {
  const onAddMock = jest.fn();
  const { queryByTestId } = render(
    <CreateForm onAdd={ onAddMock } />
  );

  fireEvent.click( queryByTestId('color_red') );
  fireEvent.click( queryByTestId('addButton') );

  expect( onAddMock.mock.calls[0].color ).toBe('red');
});

// ...the same for the rest of the colors
```

That's a simple and well-scoped test. Once we had tested all the colors individually for the `CreateForm`, we don't need to test them again for `TodoApp`. 

We can trust that the rendered `CreateForm` instance will provide no-matter-what color and check the logic without all the clicking, but be sure that the components are integrated properly:

```js
import { findInstance } from 'react-dom-instance';

it('should create an important item', () => {
  const { queryByTestId } = render( <TodoApp /> );
  const createForm = findInstance( queryByTestId('createForm') );

  // call the `onAdd` handler passed by `TodoApp` directly
  createForm.onAdd({
    text: "Buy some bread",
    color: "whatever",
    isImportant: true
  });

  // We had mocked some actions beforehand
  expect( createItem ).toHaveBeenCalledWith( "id1", "Buy some bread", "whatever" );
  expect( addToImportant ).toHaveBeenCalledWith( "id1" );
  expect( trackGAEvent ).not.toHaveBeenCalled();
});
```
In the `TodoApp` tests, we went from *"I don't care how it works internally, just click things and see"* to *"I don't care what the user clicks, I expect to receive this"*. **We are still not using the internals of the components, but we making the most of knowing their surface**, their API. 

It's not that we are not testing what user clicks, it's that we not need to repeat ourselves testing it in places that shouldn't depend on the exact user interaction. This approach has some advantages:

* Tests cases are better scoped, besides their source code.
* Changes in the `CreateForm` won't be breaking `TodoApp` tests, or, at least, we won't require multiple changes in it.
* No more big testing files for parent components with exponential growth of test cases when adding UI options.

Not that bad huh?

## When to use instances in our React tests
Splitting big test files is a nice example of how instances are handy in the way we test React components. There are other cases, like testing component's imperative methods, where they can be of help too.

But keep in mind that the fact that we can use instances in our tests, doesn't mean that we should do it everywhere. This not a matter of taste, like in *"I prefer user interaction"* or *"I prefer instances"*.

It's about finding the sweet spot where we can do deep tests of a group of components together by emulating user interaction, while we can abstract that complexity from other places, where the user interaction is not the main thing to test.

I know that saying "finding the sweet spot" doesn't help on when to use instances, but it's not that hard. We all know when a test file is getting out of control. At that moment, identify an independent unit of functionality and extract its tests to its own file. With a little bit of practice, we'll quickly learn to foresee when it's a good idea to split :)

----

This is my first article in DEV.to and I enjoyed writing it! If you liked it, follow me, give me love and unicorns, and sure I'll write much more!

My twitter is [@arqex](https://twitter.com/arqex).