Testing React with Jest and React Testing Library (RTL)
This is the note I take as I watch this course Testing React with Jest and React Testing Library (RTL) - Udemy
Introduction
React Testing Library (RTL) is a library that provides virtual DOM for React testing. Jest is a framework to run tests that is designed to be used in a CLI. In We write test using a testing function test() that takes two arguments: a description of the test and a callback function that contains the test code.
The testing processes are basically like using assertions. First we find the element on the app, then we can use assertions to check value of an element (expect(...).toBe(...)), and several other assertions are available. It throws error if the assertion fails.
Testing library recommends finding elements in testing by using accesibility handles. There are several handles, especially getByText() (but better using getByRole()) and getByTestId(). This also encourages us to make the accesibility of the app is better.
Test-driven Development
Test-driven development is a development method that creating test first before building the app. The code then will be implemented with specs from test file. The difference between TDD and BDD (Behavior-driven Development) is that TDD is focused on writing tests first, and BDD is focused on writing code first and involves a lot of people in different roles while the TDD is focused on writing tests first.
Type of tests:
- Unit tests: Test one small piece of functionality in isolation
- Integration tests: Test multiple units working together
- Functional tests: Test a functionality of how users interact in isolation (e.g. fill form and submit)
- Acceptance/End-to-end (E2E) tests: Test using actual browser and server
Examples
Get button with text “Change to blue” and expect it to be red in initial
const colorButton = screen.getByRole('button', {name: 'Change to blue'});
expect(colorButton).toHaveStyle({ backgroundColor: 'red' });
Expect button to be red when clicked
fireEvent.click(colorButton);
expect(colorButton).toHaveStyle({ backgroundColor: 'blue' });
When to do unit test
Some simple case could be covered by functional tests. But for complicated functions, unit test helps with:
- Covering all possible edge cases
- Determining what caused functional tests to fail Issue with functional test: High-level makes them resistant to refactors and makes them difficult to diagnose
ESLint and Prettier in Testing Library
ESLint: A popular linter for Javascripts. Linter is a tool that analyzes text and marks syntax that breaks rules. Linter analyzes code as written, not what happens when code is run. Linter also can adress format and style, for example, preferred assertion method.
Prettier: A tool that formats code writing such as indents, spacing, and spaces around curly braces.
ESLint has plugin for testing library and Jest DOM. Install it by
npm install eslint-plugin-testing-library eslint-plugin-jest-dom
At the root directory, create file .eslintrc.json.
{
"plugins": [
"testing-library",
"jest-dom"
],
"extends" [
"react-app",
"react-app/jest",
"plugin:testing-library/recommended",
"plugin:testing-library/react",
"plugin:jest-dom/recommended"
]
}
Configure ESLint in VSCode
Create new file in .vscode/settings.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
Add .vscode and .eslintcache to .gitignore.
If we working on VSCode, we can install ESLint plugin in VSCode. And then allow ESLint to run. If there’s red underline mark on code, it means ESLint found error. If we check the error, we can see the error message and sometimes we can quick fix it.
Configure Prettier in VSCode
In .vscode/settings.json (or settings.json)
{
"editor.defaultFormatter": "prettier",
"editor.formatOnSave": true
}
Screen Query Methods
The format is command[All]ByQueryType
command:
get: expect element to be in DOMquery: expect element not to be in DOMfind: expect element to appar async
All:
Expect more than one match. If you want to expect only one match, exclude it.
ByQueryType:
ByRole: expect element with roleByAltText: expect element with alt text (for images)ByText: expect element with text- Form elments:
ByPlaceholderText: expect element with placeholder textByLabelText: expect element with label textByDisplayValue: expect element with display value
More references on screen query on the docs
Simulating Server Response with Mock Service Worker
In testing the network stuffs like fetching data from server, we need to simulate the server response. We can use mock service worker to simulate the server response.
Setup
npm install msw
Create file mocks/handlers.js.
import { rest } from 'msw';
export const handlers = [
rest.get(..., (req, res, ctx) => {
return res(ctx.json([
{ ... },
{ ... },
{ ... },
]));
}),
]
Create file mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
Test with Mock Service Worker
import { render, screen } from '@testing-library/react';
...
test(..., () => {
render(<... />);
const scoopImages = screen.getAllByRole('img', { name: ... });
expect(scoopImages).toHaveLength(2);
// @ts-ignore
const altTexts = scoopImages.map((element) => element.alt);
expect(altTexts).toEqual([..., ...]);
})
Simulating Server Error Response
import { render, screen } from '@testing-library/react';
import { rest } from 'msw';
import { server } from '.../mocks/server';
test('handles error', async () => {
server.resetHandlers(
rest.get('http://localhost:3030/scoops', (req, res, ctx) => {
return res(ctx.status(500));
}),
rest.get('http://localhost:3030/toppings', (req, res, ctx) => {
return res(ctx.status(500));
}),
);
render(<... />);
const alerts = await screen.findAllByRole('alert', { name: ... });
expect(alerts).toHaveLength(2);
});
Testing Components Wrapped in a Provider
import { render, screen } from '@testing-library/react';
...
test(... async () => {
render(<... />);
const scoopSubtotal = ...;
expect(scoopSubtotal).toHaveTextContent(...);
const vanillaInput = await screen.findByRole(..., { name: ... });
await userEvent.clear(vanillaInput); // clear first
await userEvent.type(vanillaInput, ...);
expect(scoopSubtotal).toHaveTextContent(...);
})
Wrap isolated component in a provider
...
import { ... } from '.../contexts/...'
test(..., async () => {
render(<... />, { wrapper: ... });
})
Create custom render to wrap in provider by default
We can do this by creating test utilities and change the import statement to use the test utilities we made.
Create file test-utils/testing-library-utils.jsx
import { render } from '@testing-library/react';
import { CustomProvider } from ...;
const renderWithContext = (ui, options) => {
return render(ui, { wrapper: CustomProvider, ...options });
}
export * from '@testing-library/react';
export { renderWithContext as render };
In test file that needs custom render, change all render and screen imports.
import { render, screen } from '.../test-utils/testing-library-utils';
...
test(..., async () => {
render(<... />);
...
}