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.