- Tools and setup for testing
- Frontend testing approach
- General rules
- Goals of testing approach
- How to test
- How to get the best out of our libs
- Use
screen
when possible - [follow the recommended priority how to use queries](#follow-the-recommended-priority-how-to-use-queries)
- [make use of the jest-dom matcher](#make-use-of-the-jest-dom-matcher)
- Use
- How to structure test files
- How to group tests meaningful
- How to get the best out of our libs
- How to write test descriptions
This section offers an overview of our most used tools.
Quick links
- Jest
- react-testing-library
- jest-dom-matcher
- user-events
🔗 Jest - official documentation
We use Jest as test runner in Frontend in general and use the framework in our TypeScript tests. Jest is a testing framework with integrated test-runner for JavaScript. It can be used with Vanilla JS, ES6, TypeScript, Vue, React and so on. .
🔗 react-testing-library - official documentation
Dom testing library
is an opinionated library, which supports a specific way of writing test. It is well documented and supports us in having a consistent code base. It’s best used with jest-dom and the custom jest-dom matchers. The combination of both leads to nice readable tests!
react-testing-library is a react-adapter for the DOM testing-library.
🔗 jest-dom matchers - official documentation
To enable us to test the DOM, we use the standard JSDOM of Jest as environment. Additionally, we use the @testing-library/jest-dom
library, which provides a set of custom jest matchers (e.g. .toBeEnabled()
or .toBeChecked()
) to extend the jest matcher. You should use them for your assertions. These will make your tests more declarative, clear to read and to maintain.
🔗 user-events - official documentation
user-event
is a higher-level abstraction of fireEvent
from DOM testing library
and tries to simulate the real events that would happen in the browser as the user interacts with our components.
- We test all code.
- Test files live right beside the code they test.
- Extract functionality in pure TypeScript files where possible. They are faster and less complicated to test than React components.
Our tests...
- 🧑 confirm code works as expected for users.
- User is the consumer of the web app.
- User is a developer in the code base.
- ✅ confirm behavior and functionality for their component.
- Test every component explicitly.
- 🔎 help us to locate an error fast.
- 📕add readability to our code base.
- A dev can understand what a component does by looking only at the test.
The screen
object has every query (method used to query for elements) pre-bound to document.body. It is used like this:
import { screen } from "@testing-library/react";
// ...
expect(screen.getByRole("button", { name: "click" })).toBeEnabled();
We recommend to use screen where it is possible. It helps developer experience because you can see which queries are available on it. There are very few cases where you would need to use the container
element returned from render
.
follow the recommended priority how to use queries
A great benefit is that this can help us making our markup more accessible. If you can't use the recommended query, you should check if there is a reason for that.
Example: There is an alert, and you can't query it other than with looking for the text? That means assistive technology also has no information that this is a warning. Changing that element to have an alert role enables you to use the recommended queries. And it makes the content more accessible!
💡 Tip to remember the right query
testing-library has an experimental config. It can throw an error if there is a better query available. For example when using .getByText("click")
instead of .getByRole('button', { name: "click" })
.
It also suggests a better available query! (it suggests regex in name, so, it’s not perfect 😅, but still!)
You can make use of that by adding at top of your test file:
configure({
throwSuggestions: true
})
This adds the config only to your test file. No others are not affected! The config is making mistakes and still experimental, so we won't use it in a general config. It's up to you if you want to remove it in your test file after writing the tests.
make use of the jest-dom matcher
These matchers not only make the tests more declarative, they also support to write test in a natural feeling and user-centered way. The library is actively worked, so make sure to check out their documentation to find useful matchers for specific use-cases!
It’s a good practice to choose a precise matcher. For example .toBeEnabled()
is preferred to .toBeVisible()
if testing a (non-disabled) button in your React component.
(in combination with the queries)
it('shows a checkbox to confirm user has read the text', () => {
const checkbox = screen.getByRole('checkbox', { name: 'Confirm that I read the text' })
expect(checkbox).toBeEnabled()
expect(checkbox).not.toBeChecked()
})
getByRole
makes sure there is an accessible (for the user but also for the machine) checkbox element with an associated and readable name to it. It will fail if someone removes type="checkbox"
by accident from the input or replaces the input with a div
, which could also would affect functionality.
It’s not necessary to use toBeVisible()
on e.g. form elements. If they are not visible, getByRole
will throw an error - they are not accessible if they are not visible. (There’s a feature to actually test hidden elements by their role, but there’s almost never a need for it). Also, toBeEnabled()
is implicitly confirming that an element is visible.
.toBeChecked()
is another custom matcher from jest-dom. Only a valid, semantic and at least mostly accessible html element can be asserted with this. It can't be used on a button type="button"
for example. This implicitly make sure all these things are working correctly.
The first describe
block:
- wraps all the tests for one file
- contains only the name of the file we are testing (e.g.
MyComponent.tsx
orfancy-util.ts
)
Example React component
describe("Dashboard.tsx", () => {
// all related 'describe's, and 'it's
});
Example ts-file
describe("fancy-util.tsx", () => {
// all related 'describe's, and 'it's
});
Group your tests with nested, meaningful describe scopes.
Jest offers global methods to create blocks of tests. The most used for us are describe and it (it is an alias for test).
`describe` creates a block that groups together several related tests.
`it` is the method that actually runs one test.
Grouping several it
in a describe
block and nesting describe
blocks create a more structured test file. It also translates to a nicely readable output in the test runner.
A describe
block describes a scenario or test case. An it
block describes the specification of different assertions in this scenario. A rule of the thumb for unit tests is to only have one assertion (expect) in one it
block. This helps us find the cause of a failing test faster. Grouping multiple it
blocks in one describe
makes it easier to archive this.
The nesting with describe
blocks should ot be overused.
️✅ Do
describe('nameOfFunction', () => {
it('returns true when user chose "cat"', ...)
it('returns true when user chose "dog"', ...)
})
⛔️ Don't
describe('nameOfFunction', () => {
describe('when user chose "cat', () => {
it('returns true', ...) })
describe('when user chose "dog"', () => {
it('return false', ...)
})
})
describe('MyNiceComponent.tsx', () => {
describe('renders a page with default props', () => {
it('shows a headline', () => {
...
})
it('shows an input field', () => {
...
})
it('shows a disabled button', () => {
...
})
})
describe('now there are more tests', () => {
})
})
This generates the following output when running the tests:
MyNiceComponent.tsx
shows a page with default props
✓ renders a headline
✓ renders an input field
✓ renders a disabled button
describe("MyNiceComponent.tsx", () => {
//...
describe("user can fill out the email field and send the form", () => {
it('shows a disabled "Send" button while the input is empty', () => {
//...
});
it("enables user to fill out input field", () => {
//...
});
it('shows an enabled "Send" button when user filled out input', () => {
//...
});
it("enables user to send the form with their input", () => {
//...
});
});
describe("now there are more tests", () => {
//...
});
});
The test output now looks like this:
MyNiceComponent.tsx
shows a page with default props
✓ shows a headline
✓ shows an input field
✓ shows a disabled button
user can fill out the email field and send the form
✓ shows a disabled "Send" button while the input is empty
✓ enables user to fill out input field
✓ shows an enabled "Send" button when user filled out input
✓ enables user to send the form with their input
We should use a consistent approach and language in our test description. This help to understand and document the code they are referring to!
🔖 You can read more in the article Art of test case writing.
Use the describe
block to describe a scenario or test case.
This block can:
- describe a larger block of functionality.
- contain a set of specific requirements for one test case.
- group one user-path.
Examples:
- describe "renders all necessary elements"
- describe "when user selects a certain environment"
- describe "handles the form for deleting a topic"
The it
block contains the specification itself. Together with its assertion it describes the expected outcome. Keep in mind that one-assertion-per-it is a good rule to follow for small tests.
We read the it
together with the description itself:
it("closes the modal when user confirms");
Is read as "it closes the modal when user confirms"
Examples:
- it "shows an input element"
- it "changes the color when user clicks button"
- it "disables the button when required input is missing"
In general: Uncomplicated, user-centered language without unneeded implementation-related context makes test readable. It helps others to understand your code better. Keep in mind that not all people are native english speaker!
It’s shorter, clearer, more emphatic and to-the-point.
️✅ Do
it("filters a list based on user input")
⛔️ Don't
it("a list is filtered based on string submitted by the user")
It’s a good way to express general truths or unchanging situations in an easy-to-read way.
️✅ Do
it("warns the user when they click delete")
⛔️ Don't
it("will warn the user when they clicked delete")
Don’t use modal verbs! Make direct and unambiguous statements about the expected outcome. The test is not to make sure code should (hopefully) do something. The test makes sure the code does this.
️✅ Do
it("shows list in the right order")
- ⛔️ Don't
it("should show list in the right order")
For React component tests, try to focus on a more user-based language. Don’t add unnecessary implementation details to the descriptions.
️✅ Do
it("shows a list of topics filtered by team")
⛔️ Don't
it("should call the getTopicByTeam api to fetch new list items")