Resources
A collection of useful resources, some of which are repeated in the guide below. If it's not clear what all these terms mean, look to the guide.
Matchers
Queries
Triggering events
Best practices
Guide
Jest
Jest is a test runner. It provides the describe
and it
/test
(these two are interchangeable) functions. It also provides functions like beforeAll
, beforeEach
, afterAll
, afterAll
in case you want to perform some actions before each of your tests (i.e. before each it
block) in a given describe
block, or once before all of them, or after each, etc. We use jest to run all of our tests using npm run test
(this script calls jest).
Matchers
Jest also provides matchers like .toEqual
which are check whether some value meets a condition. If we want to assert a particular statement we write something like expect(number).toEqual(4)
. You can also negate a matcher: expect(number).not.toEqual(4)
.
As a technical point, note that these matchers are methods on the object returned by the function expect
which is provided by Jest.
React testing library
Jest is a general test runner that can be used for any javascript code. React testing library implements helpers to make the specific case of testing a React app easier.
Accessing components
With React testing library you access things on the "page" through queries (there is not actually a page during the test, instead a simulated browser page called the virtual DOM is created in the jsdom
testing environment that we selected in our jest config).
The philosophy of React testing library is to simulate the way that a user would use the app during testing. This means accessing components by searching for their text or some visual component the way you would when interacting with a webpage. For example if we wanted to do something with a submit button that had the text 'Submit', instead of doing querySelector() for some css selector that would identify the button, we use utilities from React testing library to find it by its text. In this case we might do
const submitButton = screen.getByText('Submit');
where .getByText
is the query that we're using. We can provide a string or regular expression to a query.
React testing library provides recommendations on which queries to use:
That page also include a summary table of the types of queries provided by React Testing library.
Some important helpers
render
: This is used to render your component to the testing environment just the way that in our app we render a component to the document. It substitutes for React's render
function during tests.
screen
: This serves as a substitute for document
during the test. Whereas in a webpage you might do document.querySelector()
to find an element, in a test you would do screen.getByText()
or some other query method.
Additional matchers (jest-dom)
There is a companion library to React testing library called jest-dom
which extends the matchers from Jest to include matchers that are more useful for tests on a "webpage" (or more accurately the virtual DOM). These include matchers like toBeVisible
or toContainElement
. See below for the full list:
Simulating user interaction
React testing library supplies the basic function fireEvent
to simulate user interaction, but recommends that the companion library user-event
be used instead for simulating user events. user-event
provides a more faithful recreation of the kinds of event that actually occur in the browser and so can help catch bugs that might otherwise slip through.
Testing layout
We can use this naming convention to structure our test files.
- Root describe: the name of the component you're testing
- Nested describe (you probably won't even need these): must have the
when
prefix to indicate a specific case (i.e. 'when user exists'
)
- Test in an
it
block: a sentence describing the behavior
Here's an example from the Carousel tests:
describe('Carousel', () => {
it('should show the correct number of items', () => {
// test goes here
});
it('should scroll on button click', () => {
//test goes here
});
});
Getting started
When you're getting started on a component you can fill out your test files with todos that will test for all the specifications for that component:
describe('Component', () => {
it.todo('should meet specification #1');
it.todo('should meet specification #2');
// ...
});
Note that you can't provide a 2nd argument (the testing callback) to an it.todo
or you'll get an error. If as you're working you have a broken test that you don't want to run, you can change 'it(...)
to it.skip(...)
.
Testing example
Suppose we have a Component
component that contains two buttons. One of the buttons says 'Click Me!' on it and the other says 'Enabled' on it. When you click the 'Click Me!' button the text on the other button should change to 'Disabled' and the button itself should become disabled. The implementation of Component
does not actually matter for how we write the test, as you'll see.
- First we have to import our component and
screen
from React testing library (due to an issue with global importing of screen
that I was unable to resolve). You won't need to import any of the other testing utilities since that is taken care of in a test setup script that runs before each test. If there's anything additional you need to import that you think would be useful across our test suite, add it to the test setup script (client/src/tests/testSetup.js
).
import Component from './Component.jsx';
import { screen } from '@testing-library/react';
- Arrange: We need to set up the elements that we are going to test. We use
render
to actually add the element to the virtual DOM. Note that this is actually the render
from React testing library, not ReactDOM.
describe('Component', () => {
it("'Click Me!' button should disable the other button on click", () => {
render(<Component />;
// rest of testing code will go here
});
});
- Act: Now we simulate user interaction. We need to make some modifications to our code above to trigger simulated events:
describe('Component', async () => {
it("'Click Me!' button should disable the other button on click", () => {
const user = userEvent.setup();
render(<Component />;
// rest of testing code will go here
});
});
Now we can trigger our event:
await user.click(screen.getByRole('button', 'Click Me!'));
We use getByRole as recommended by the query priorities, although this requires that you use the type="button"
attribute on your button to provide adequate accessibility. If you don't do that you can instead do screen.getByText('Click Me!')
.
- Assert: Finally, we use matchers to check that the proper behavior has been achieved.
expect(screen.getByText('Disabled').toBeDisabled();
In all, this would be the test
import Component from './Component.jsx';
import { screen } from '@testing-library/react';
describe('Component', async () => {
it("'Click Me!' button should disable the other button on click", () => {
const user = userEvent.setup();
render(<Component />;
await user.click(screen.getByRole('button', 'Click Me!'));
expect(screen.getByText('Disabled').toBeDisabled();
});
});
Fixing a test
The most useful tool might be screen.debug()
. Put this in your test somewhere and it will console.log
the state of the virtual DOM at that moment.