Escape the Mud

Write better tests and code with React Testing Library

February 23, 2020

What is the purpose of the tests I am about to write?

For me, when I am testing my React code, the purpose of the tests I write is to make sure that the user interface that I am providing to my consumers of my website behaves the way that I intend it to.

If that’s what I care about then I think it makes sense to test my user interface from a users perspective.

Let’s quickly think about some of the ways a user can interact with a website. We will use a counter as an example.

Let’s imagine there is some text that indicates what the current count is, starting at zero, and a button that increments the count by one every time I click it. Here is a working example just incase your imagination is tired 😴

Let’s think about the way I would interact with this view.

  • I visually identify what the current count is (0).
  • I visually identify the increment button.
  • I click the increment button.
  • I visually identify that the current count has increased by 1.

Those seem like really basic steps, but basic steps translate very well to tests.

Using the same example let’s now think about some of the ways a user cannot interact with a website

  • I cannot find the current count by it’s class name.
  • I cannot call setState on an instance of my React component to increment the count.

Now let’s look at some tests written in Enzyme

import React from 'react';                                                      
import { shallow } from 'enzyme';                                               

import { Counter } from '../Counter';                                           
                                                                                 
describe('Counter', () => {                                                     
  const subject = shallow(<Counter />);                                         
                                                                                 
  it('increments', () => {                                                      
    expect(subject.find('.current-count').text()).toEqual(                      
      'The current count is: 0'                                                   
    );                                                                          
                                                                                
    subject.setState({ count: 1 });                                             
                                                                                 
    expect(subject.find('.current-count').text()).toEqual(                      
      'The current count is: 1'                                                   
    );                                                                              
  });                                                                                 
});

Looking at these tests you might already see all the problems with it but bear with me 😉

First things first. This test knows about the class name applied to our span counter element. That might not seem like a big deal but it means that if someone decides to change a class name then your tests break.

When I see tests break that means something is wrong with my code

But would there be anything wrong with this component? It still displays the correct count to the user and increments properly. We can argue that maybe the CSS being applied to this component is no longer correct but we aren’t testing CSS here are we?

Looking further we can see that the button is never clicked and instead we just manually update the state. (subject.setState({ count: 1})).

This test doesn’t even check that clicking the button works. You might respond with something like: “The developer should have written better tests”, but I would ask “Why is the developer even given the ability to write these kinds of tests?“.

By giving the developer the ability to manually update the state we hand over a massive amount of trust to them. The possibility exists that the component could never end up in the state the tests / developer placed it in from a user interacting with it.

Let’s compare that with some tests written in React Testing Library

import React from 'react';
import { render, fireEvent } from '@testing-library/react';

describe('Counter', () => {
  const subject = render(<Counter />);

  it('increments', () => {
    const { getByText } = subject;

    expect(getByText('The current count is: 0')).toBeDefined();
    fireEvent.click(geByText('Increment'));
    expect(getByText('The current count is: 1')).toBeDefined();
  });
});

By comparism these tests make some notable changes.

  • I am no longer finding things by class name.
  • I am updating the state by interacting with the user interface instead of an instance of my React Component

Overall React Testing Library gives you a lot fewer tools than enzymeto write tests with.

This forces developer to write tests from more of a user perspective. Another bonus from the lack of tools that I’ve experienced is it forces you to write more accessible code.

Since you can’t find things by class names and instead need to use a limited amount of tools to find elements you need to make sure you can find your elements.

Imagine an img element without an alt attribute being consumed by a screen reader.

The alt attribute provides alternative information for an image if a user for some reason cannot view it (because of slow connection, an error in the src attribute, or if the user uses a screen reader).

The screen reader cannot get information about that image and if you don’t add an alt attribute then you’ll have difficulty finding it with React Testing Library too.

With Enzyme we could just continue on and query by class name and continue being oblivious to accessibility.


Escape the Mud is a developer centric blog that covers a wide array of topics. Written by Dennis Marchand