TBGRT Dark

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 26

Page 1 of 25

Table of Contents
Changelog

v1.0.0 - 2021/02/18

Introduction

How do I get started testing?

Let's integration test a React form!

Setup

Steps

So how do we get here?

There was a lot going on in that example...

What are all the React testing tools and how do I use them?

Jest

Setting Jest up

Enzyme

React Testing Library

Cypress

Which testing framework should I use (Enzyme or React Testing


Library)?

React Testing Library for new stu

Avoiding worst-practices in Enzyme

Page 2 of 25
Convincing your team to migrate

What are the di erent types of tests we use in React?

Unit Testing

Integration Testing

End to End Testing

Solutions

Snapshot Testing

Visual Regression Testing

Solutions

How do I make testing a habit?

Use feedback loops

Run all of your tests as part of CI

What's a good level of code coverage?

100% code coverage isn't required

What to actually aim for

Should I be doing test-driven development (TDD)?

What I mean by TDD

It's not for me

Don't feel forced to use TDD

Final Thoughts

Page 3 of 25
Changelog
v1.0.0 - 2021/02/18
Initial release

Page 4 of 25
Introduction
Welcome to The Beginner's Guide to React Testing.

The React ecosystem changes quite a bit, and if you're not actively on Twitter,
reddit, or following several newsletters, it can be pretty di cult to keep track of
what the latest best practice is.

As a member of the React community on reddit, I notice the same questions


about testing pop up almost weekly, so this guide is an attempt at answering
them in one place (I also intend to update this book as new questions come up).

If you think your friends or colleagues would bene t from this book, absolutely
feel free to share it with them.

If you still have questions after you read this guide, feel free to email me at
max@maxrozen.com and Tweet at me @rozenmd.

If you like what you read, you can nd more of my writing at MaxRozen.com.

Page 5 of 25
How do I get started
testing?
It can be pretty di cult to work out what exactly you should be testing when it
comes to writing tests in React. Particularly when half of the examples in
articles online seem to be doing it "the old way", rather than testing
functionality over implementation details.

Typically though, when I start writing tests for a new project, I start testing
entire screens where possible (assuming I'm working with a well-tested
component library), meaning I start with integration tests rst.

Let's integration test a React form!


We're going to be testing a form almost every public web app you're going to
build has: a Login form. It's probably one of the most important parts of your
app (in terms of business value), so let's be con dent it actually works!

Page 6 of 25
Our React login form for testing

Setup

We're going to be using create-react-app, because it comes bundled with


@testing-library/react (also known as React Testing Library). I'm also using
react-hook-form to build our form, because it's the fastest way I know to build
a form in React apps.

STEPS

1. Clone this repo

2. Run:

yarn
# then
yarn start

Page 7 of 25
You should see something like this:

React login form after starting

3. At this point, if you ran yarn test , you would see the following:

PASS src/pages/Login.test.js
✓ integration test (177ms)

Test Suites: 1 passed, 1 total


Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.134s
Ran all test suites.

Watch Usage: Press w to show more.

SO HOW DO WE GET HERE?

First o , here's the code from our integration test:

Page 8 of 25
import React from 'react';
// fireEvent lets us click on our `Button` component
import { render, fireEvent, screen } from '@testing-
library/react';
// user-event lets us type into our inputs
import user from '@testing-library/user-event';
import Login from './Login';

// Test is async to enable us to use `findByText()`


// once the form submits
test('integration test', async () => {
const USER = 'some-username';
const PASS = 'some-pass';

render(<Login />);

// using regex in getBy and findBy


// functions, so it's case insensitive
const userInput = screen.getByLabelText(/username/i);
user.type(userInput, USER);
const passwordInput = screen.getByLabelText(/password/i);
user.type(passwordInput, PASS);
const submitButton = screen.getByText(/submit/i);

fireEvent.click(submitButton);
// We use `findByText()` after performing an action
// that may be asynchronous (like filling out a form).
//
// findByText returns a Promise, letting us await
// until it finds the text it's looking for before continuing
expect(await screen.findByText(/your
username/i)).toBeInTheDocument();

Page 9 of 25
expect(await screen.findByText(/your
password/i)).toBeInTheDocument();
});

The above example likely looks complicated if you're new to integration testing,
but we've essentially taken the steps a user takes when logging in, and turned it
into a test:

1. Load the Login screen

2. Click on the username input, and type the username

3. Click on the password input, and type the password

4. Click the Submit button

5. Wait around for some sign that the Login worked

We've built a test that can type into our TextField components, click on our
Button component, and trigger the Form component's onSubmit function,
without ever referring to implementation details!

If you're confused about findByText vs getByText , don't worry - that's


normal. I typically have to look this up each time I use them, but ndBy
functions are for use after async actions (like clicking the Submit button), and
getBy functions are for general use.

React Testing Library also has a cheatsheet with tips to help you check which
function to use.

THERE WAS A LOT GOING ON IN THAT EXAMPLE...

That repo used quite a few libraries:

Jest

Page 10 of 25
React Testing Library

React Hook Form

It also introduced you to testing functionality instead of implementation


details.

This chapter covered the "how" of testing React, the following chapters explain
the "what" and the "why".

Page 11 of 25
What are all the React
testing tools and how do I
use them?
Jest

Jest in the console

Jest is a test runner that runs your tests in your terminal, with a number of
useful options for how to run your tests.

Setting Jest up

If your project uses create-react-app (you can check by looking in your


package.json for react-scripts as a dependency), you don't need to follow these
steps.

You can add jest to your project by running:

Page 12 of 25
npm install --save-dev jest
# OR
yarn add --dev jest

Then add the following script to your package.json's scripts section:

{
"scripts": {
"test": "jest"
}
}

To get Jest to work with React, you'll want to also follow the steps here.

From here, you can run yarn test to have Jest run your tests once, or run
yarn test --watch to enable watch mode, which runs tests only for les that
you're actively editing. This gives you a nice feedback loop when you're writing
code, and trying to avoid break things.

Jest by itself can't actually load React components, which is why Enzyme and
React Testing Library exist.

Enzyme
For a very long time, Enzyme was the library React developers used to test React
components. It lets you mount your components, and manipulate them for your
tests.

As a result of being around for so long, most legacy React projects likely still use
Enzyme.

Page 13 of 25
The issue with Enzyme is that it provides several APIs that encourage testing of
implementation details (such as shallow() , find() , instance() and
state() ), rather than functionality (can the user click our UI, and does the UI
change as a result?).

Eventually, a testing library that restricted developers to encourage the testing


of functionality emerged, and it was called React Testing Library.

React Testing Library


React Testing Library is di erent to Enzyme, in that it avoids focusing on
testing implementation details (such as a component's state, a component's
methods, a class component's lifecycle methods, or child components).

Instead, React Testing Library's family of packages guides developers towards


testing their UI in a more user-centric way (which is a fancy way of saying,
instead of testing and checking a component's props/state, your tests click
around your app, and try perform actions your users might perform).

Cypress
Cypress is an all-in-one end-to-end testing solution (we'll discuss what end-
to-end testing is in a later chapter) - meaning it's both a test runner (like Jest),
and a set of APIs for testing (like React Testing Library).

Cypress launches a web browser, and actually clicks through your app. It
requires quite a bit more set-up to work properly than other test tools (such as
special test users, test instances for your app), though it also gives you
con dence that your app actually works.

On top of testing your entire web app from your computer, Cypress also
supports testing from their cloud as part of your app's CI/CD.

Page 14 of 25
Which testing framework
should I use (Enzyme or
React Testing Library)?
React Testing Library for new stu
For new projects, I would recommend using React Testing Library (RTL). This is
made easier by create-react-app, which now bundles RTL, and requires no
setup to start testing.

One thing you might want to install if you're setting up React Testing Library is
React Hooks Testing Library. It's made by the same people as React Testing
Library, with the same concepts, and makes testing React Hooks signi cantly
less complicated than other solutions.

Avoiding worst-practices in Enzyme


Typically though, we use the testing framework that's already in the app we're
working on. For most companies, that's still going to be Enzyme.

If you're stuck using Enzyme, the best you can do is avoid testing
implementation details when using it (which will be a challenge, if you're new
to testing).

Particularly:

Use mount() where possible over shallow()

Rather than checking props and state, try to think of what your user can see,
and check that

Page 15 of 25
Try to avoid blindly copying existing Enzyme tests when writing your own
tests. They'll often follow outdated practices such as testing
implementation details (we didn't know any better at the time, sorry), and
won't actually improve your con dence that the app does what you expect.

Convincing your team to migrate


Part of the skillset of a more-senior engineer is to be capable of discussing pros
and cons of di erent solutions in an e ort to convince someone of the merits of
your solution.

If you're particularly keen on using React Testing Library, it could be worth


trying to convince your team to adopt it for new tests only at rst, and start
migrating tests across to the new framework over time.

In the past I've managed to convince teams I work on to completely change


their tooling (like moving to TypeScript), because the existing solution wasn't
stopping us from making silly mistakes. You might be able to make a similar
argument to get your team to incrementally adopt React Testing Library.

My recommendation for these sorts of migrations is typically:

For all new code, we'll use the new solution

For all old code, we'll leave the old solution as-is, unless we need to update
existing functionality in the old code. In other words, if you're editing an
existing le, take the e ort to refactor it to the new way of doing things.

Page 16 of 25
What are the different types
of tests we use in React?
The world of software testing has a lot of di erent tests to choose from. Luckily
for us in React, we typically only use three: Unit testing, Integration testing,
and End-to-end (E2E) testing.

I'll also cover snapshot testing and visual regression testing, although I
consider them to be forms of unit testing.

Unit Testing
Unit testing involves testing a unit (or chunk of code) in isolation, to gure out
whether it does what we expect. In React, typically the "unit" refers to a utility
function, function component, or class component.

Here's an example of a unit we would typically write unit tests for :

function convertScoreToColour(score) {
const BAD = '#FF0000';
const AVERAGE = '#FFFF00';
const GOOD = '#00FF00';

if (score < 50) {


return BAD;
} else if (score < 90) {
return AVERAGE;
} else return GOOD;
}

Your actual tests would then look like this:

Page 17 of 25
describe('convertScoreToColour', () => {
test('Numbers < 50 return the BAD colour', () => {
let output = convertScoreToColour(50);
expect(output).toBe('#FF0000');
});
test('Numbers > 50 but < 90 return the AVERAGE colour', () =>
{
let output = convertScoreToColour(75);
expect(output).toBe('#FFFF00');
});
test('All other numbers return the GOOD colour', () => {
let output = convertScoreToColour(92);
expect(output).toBe('#00FF00');
});
});

Despite this being a contrived example (that could use some refactoring),
there's quite a bit of bene t you can already get from testing a function in this
way.

For example, if we expect our function to only handle numbers less than 100,
we could write a test to check what happens when the number is greater than
100:

test('Numbers > 100 return nothing', () => {


let output = convertScoreToColour(101);
expect(output).toBe('');
});

This test example would fail (the code we wrote would return the good colour in
this case), so you'd have to now update the function to match the behaviour you
expect in your tests.

Page 18 of 25
(PS: This example, where we wrote our test for numbers greater than 100 before
writing code is a form of test-driven development, which we'll cover in a later
chapter.)

In earlier versions of React, before React Testing Library became popular, we


would test React components in this way too - load up a component, call a
function with certain inputs, and check that the output matches what we
expect. Over time, developers found that tests like these didn't give us much
con dence when using several components together, so we started focusing
more on integration testing.

Integration Testing
It helps to think of integration testing like a bigger unit test, except the unit
you're testing is the combination of several smaller components.

More concretely, instead of just testing a Button component, or a TextField


component by themselves, an integration test ensures that several TextField
components placed next to a Button component, within a Form component,
behave as we expect.

See the rst chapter - How do I get started testing? for a practical example of
integration testing a form.

End to End Testing


End-to-end (E2E) tests involve testing a whole user journey, or ow through
your app, end-to-end (hence the name).

For example, a typical E2E test may involve logging into your app, clicking
through a few screens, creating a few records, viewing them, deleting them,
then logging out.

Page 19 of 25
Due to how long E2E tests take to run and develop, we tend to only write E2E
tests for what some developers call "happy paths". Happy paths are the
journeys your users take through the app when everything goes right - no
errors, no warning messages, just checking that performing the correct steps
results in the correct response from the system.

Solutions

There are a few solutions in the E2E space worth checking out, particularly
Cypress and TestCafé.

There are pros/cons of each tool, depending on what sort of React app you work
on. The best way to know which one is for you is to try both, and see if it meets
your use case.

Snapshot Testing
Snapshot testing typically means taking a React component, rendering it to
string/HTML (so that we have text to save), and saving it as a baseline "good"
version. The next time you run your tests, your test runner (typically Jest)
compares the baseline version against what the React component looks like
now. If the two versions are the same, the test passes, otherwise it fails, and the
snapshot test needs to either be updated, or you accidentally updated the React
component and you need to undo your change.

For a short period of time, the React community considered snapshot testing
React components as a "Good Idea". It wasn't until most of our colleagues
blindly updated snapshots every time they changed that we realised perhaps it
wasn't such a good idea after all.

In a React app of hundreds of components, Snapshot Fatigue became a thing,


and when every change to your codebase resulted in a snapshot needing to be

Page 20 of 25
updated, people just stopped checking what the changes were.

Visual Regression Testing


Over time, Visual Regression (VR) testing started to emerge as a potential
replacement to snapshot testing.

Visual Regression testing is a form of snapshot testing, except instead of


comparing two snapshots of text, you compare two di erent images with the
di erences highlighted by the visual regression testing service.

Used sparingly (for example, only for your building block components or
component library), visual regression testing is a powerful tool to avoid
unintentional changes within your design system or component library.

Solutions

There are a few solutions in the Visual Regression testing space as well - I've
personally used Chromatic for a design system in the past, and it saved me
hours of manual checks each week.

There's also Percy.

As always, I recommend giving both solutions a trial, and seeing which one you
like best.

Page 21 of 25
How do I make testing a
habit?
Use feedback loops
My favourite part of testing in React is the feedback loop I get from running Jest
in watch mode. By passing Jest the --watch ag (so yarn test --watch ), I'm
able to have jest only run tests for les that I change, and in doing so, I know in
the moment when I've broken something.

Run all of your tests as part of CI


This assumes your team's project uses CI (which it should), but running all of
your tests - both on the branch you're working on, and the main branch helps
ensure your code works as you expect before deploying.

You might be wondering how this helps build a testing habit - the trick is, you
won't be able to deliver code if your tests aren't passing, so it forces you to run
tests as you code.

Page 22 of 25
What's a good level of code
coverage?
100% code coverage isn't required
As a developer, you might see some libraries in the JavaScript ecosystem with
100% code coverage. You might then wonder, if libraries aim for 100% code
coverage, why don't I?

Let me start by saying: Don't let coverage stop you from writing tests. You
might be thinking "Oh, but I'll never reach 100% code coverage, what's the
point?", but realistically, any tests are better than no tests.

For the apps we build day to day, 100% code coverage is often unrealistic, and
su ers from diminishing returns (as in, you'll get less bene t per test written
once you pass a certain percentage of code covered). As well as this, it's possible
to reach 100% code coverage without actually testing e ectively (particularly
when using Enzyme and shallow() ).

What to actually aim for


Don't use code coverage as an arbitrary milestone. Instead, write high quality
tests for your most valuable features and components.

Think about it this way, if you were a user for the web app you build, what
functionality do you use every day, and would get upset if it stopped working?
That's the functionality you need to have covered by tests.

If you then hit 100% code coverage as a result of testing all of your app's
functionality - great! But don't let the number be your focus.

Page 23 of 25
Should I be doing test-
driven development (TDD)?
What I mean by TDD
To me, TDD means writing tests which fail before you start writing any code.
Once the tests are written, you start writing the code that makes the tests pass.

All of the tools described in this book support both a TDD and a non-TDD
approach.

It's not for me


I personally don't use TDD in my development work ow, as it doesn't t with
my mental model of how I code (I prefer rapidly iterating on a component, then
at completion I write my tests).

If it works for you, great! I'd highly recommend giving it a try, to see if you like
it - but don't worry if you feel that it's not for you.

Don't feel forced to use TDD


I've rarely found companies require their frontend developers to use TDD (the
ones that did also required quite a few other things, a huge red ag in my
opinion).

Page 24 of 25
Final Thoughts
That was the last chapter in the guide. I hope The Beginner's Guide to React
Testing was useful for you to get started.

As this book itself is version controlled (as all of my writing is), I'm always keen
to hear readers' thoughts - whether it's on how it can be improved, or general
comments.

If you'd like to get in touch with me, email and twitter are your best bets.

At the risk of sounding like a YouTuber by asking you to like and subscribe, I
also invite you to visit my website, to read more about React (and potentially
more web development topics in the future). You can also subscribe to my
newsletter to receive an article every two weeks with a single tip or trick to help
you in your career as a developer that works with React.

Thank you very much for reading The Beginner's Guide to React Testing, feel
free to share it with your colleagues, and discuss its ideas.

Cheers, Max.

Page 25 of 25

You might also like