docs/react-wiki-archive/BestPractices/Testing-with-Jest.md
๐จ๐จ This page is primarily about
@fluentui/react-components("v9") and@fluentui/react("v8") and related packages. ๐จ๐จย See this page for@fluentui/react-northstar("v0").
Fluent UI's unit, functional, and snapshot tests are built using Jest. This allows us to run tests in a Node environment but simulates the browser using jsdom. (See this page for other types of tests.)
We use various libraries on top of Jest for rendering and interacting with React components:
Our Jest setup generally require that packages be built before testing, so before running tests the first time, you should run yarn build --to my-package from the repo root.
To run the tests for one package, cd to that package and then run yarn test.
To run an individual test (or technically any tests with a path matching this substring), cd to the relevant package and run yarn jest MyTestName.
(You can also run tests for the whole repo by running yarn test at the repo root. This will build beforehand if needed.)
When you are developing tests, use the watch mode to run the tests as you write them!
yarn start-test, or for v9, yarn test --watchThe repo includes launch configurations for debugging tests using Visual Studio Code. (You could also configure debugging in another editor of your choice.)
*.test.ts or *.test.tsx)Tests in Jest are written similarly to Mocha tests, though Jest includes a number of assertions that work similarly to Chai. (If you've never used either of those, don't worry about it!)
expect matchersjest object including fake timer APIs (more info)Note that you do not need to import the assertions or the Jest APIs; they should be available automatically through the included typings.
A basic test example:
describe('thing', () => {
it('does something', () => {
expect(thing()).toEqual(aValue);
});
});
In cases where you need to automate a component and validate it performs correctly, you can use React Testing Library to mount components, evaluate DOM structure, and simulate events.
Here's a very basic example of what a test might look like (we'll go over this in more detail later):
const onClick = jest.fn();
const { getByRole } = render(<Button onClick={onClick}>This is a button</Button>);
userEvent.click(getByRole('button'));
expect(onClick).toHaveBeenCalled();
The guide on migrating from Enzyme nicely sums up Testing Library's philosophy, and how it differs from the way tests tended to be written with Enzyme:
The primary purpose of React Testing Library is to increase confidence in your tests by testing your components in the way a user would use them. Users don't care what happens behind the scenes, they just see and interact with the output. Instead of accessing the components' internal APIs or evaluating their state, you'll get more confidence by writing your tests based on the component output.
Some examples of this philosophy:
className, id, or DOM structure. This approach helps encourage writing UI that's accessible to all users.@testing-library/user-event more realistically simulates the full sequence of events that occur during user interaction.Testing Library's APIs also make it easier to write realistic, user-focused tests compared to Enzyme.
This page will only go into basics of using Testing Library (and some common pitfalls).
For a more detailed run-through of how to write tests, check out this tutorial recommended in Testing Library's docs. (One difference: we typically use the queries returned by render(), rather than the global queries from screen like the tutorial uses.)
It's highly recommended that you check out Testing Library's docs as well!
render() API detailsfireEvent for firing raw events, and considerations for fireEventuserEvent (@testing-library/user-event) for simulating interaction event sequencesimport * as React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('calls a function on click', () => {
const onClick = jest.fn();
// render() returns a variety of get/query methods
const { getByRole } = render(<Button onClick={onClick}>This is a button</Button>);
// getByRole will throw if it doesn't find anything
const button = getByRole('button');
userEvent.click(button);
expect(onClick).toHaveBeenCalled();
// ๐งน Unlike with Enzyme or react-test-renderer, cleanup is automatic!
});
});
๐จ Prefer specific unit/functional tests over snapshot tests. For example, if you want to verify that an attribute is applied in a particular place, it's much better to test that directly rather than using a snapshot.
Jest allows creating snapshot tests that compare an object or text with expected output.
expect.toMatchSnapshot(...) abstracts loading a .snap file for the test to compare, or will create one if none existsexpect.toMatchInlineSnapshot() saves the snapshot within the test file itself
Snapshots in v8 will include style information from @fluentui/merge-styles CSS classes. Snapshots in v9 do not include style information.
// Foo.test.tsx
import * as React from 'react';
import { render } from '@testing-library/react';
const Foo = () => <div>hello world</div>;
describe('Foo', () => {
it('renders correctly', () => {
const { container } = render(<Foo />);
// This saves the snapshot in a file ./__snapshots__/Foo.test.tsx.snap
expect(container).toMatchSnapshot();
});
it('renders correctly (inline snapshot)', () => {
const { container } = render(<Foo />);
// Inline snapshots are good for basic cases
expect(container.firstElementChild).toMatchInlineSnapshot(`
<div>
hello world
</div>
`);
});
});
If you ever break a snapshot, you can update it by running one of the following in the package folder:
yarn update-snapshotsyarn test -uSee also Tips for interaction testing above.
.toBeDefined() and .not.toBeNull()The matcher expect(value).toBeDefined() checks that value is specifically undefined. This can be a problem when null is also a possible return value (such as with many DOM APIs) because null is not undefined.
// โ querySelector and queryBy____ return null if not found, null !== undefined, matcher passes
expect(queryByRole('button')).toBeDefined();
expect(element.querySelector('.foo')).toBeDefined();
Similarly, expect(value).not.toBeNull() will succeed if value is undefined.
Instead, consider either checking for a specific value if possible, or using expect(value).toBeTruthy(). This works unless 0, '', or false is a possible "success" return value (in which case you should check specifically for that value instead).
// โ
return value is always an object (matcher passes) or null (matcher fails)
expect(queryByRole('button')).toBeTruthy();
expect(element.querySelector('.foo')).toBeTruthy();
// also okay if you're CERTAIN that the method will never return undefined on failure
// (you can assume this from DOM or testing-library APIs if documented as such)
expect(queryByRole('button')).not.toBeNull();
expect(element.querySelector('.foo')).not.toBeNull();
Using browser methods like getBoundingClientRect won't work when using enzyme to render a document fragment. It's possible to mock this method out if needed; see the FocusZone unit tests as an example.
If the test is using userEvent.type(element) and throws an error since getBoundingClientRect is missing, you can provide the option userEvent.click(element, { skipClick: true }) to skip the step of clicking on the element (if its actual size/position is irrelevant to the rest of the test).
There are a few possibilities here:
@testing-library/user-event to more realistically simulate the sequence of user events that would occur in a real interaction.getByRole can't find an element or says it's hiddentesting-library's getByRole will only return elements that would be included in the accessibility tree (per its heuristics). Some possible reasons why an element would register as hidden:
getByRole for convenience when it doesn't quite apply)If you need to override this, you can pass { hidden: true } as a second argument to getByRole to include the hidden elements. Example: getByRole('button', { hidden: true })