Skip to content

Testing with Jest

Fluent UI Team edited this page Apr 9, 2022 · 5 revisions

🚨🚨 This page is primarily about @fluentui/react-components ("v9") and @fluentui/react ("v8") and related packages. 🚨🚨  See this page for @fluentui/react-northstar ("v0").

Overview

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:

Running tests

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.)

Running tests in watch mode

When you are developing tests, use the watch mode to run the tests as you write them!

  1. Go to the package folder where you want to run the tests.
  2. The start command varies by library: for v8, yarn start-test, or for v9, yarn test --watch
  3. Edit and saving tests should now cause the console to re-run the tests you have added/modified.

Debugging

The repo includes launch configurations for debugging tests using Visual Studio Code. (You could also configure debugging in another editor of your choice.)

  1. Set breakpoints in the test file (*.test.ts or *.test.tsx)
  2. Open the Run (debugger) pane in the sidebar and choose the configuration you want: usually Debug current open test to run only the current open test, or Debug test to run all tests for the package the current file is in
  3. Start debugging

Writing tests

Simple unit testing

Tests in Jest are written similarly to Mocha tests, though Jest includes a number of assertions that work similarly to Chai.

A basic test example:

describe('thing', () => {
  it('does something', () => {
    expect(thing()).toEqual(aValue);
  });
});

Note that you do not need to import the assertions or the Jest APIs; they should be available automatically through the included typings.

Functional testing

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.

React Testing Library emphasizes testing the UI in the way your users would interact with it, rather than relying on and testing implementation details. Even if you've tested React components before, it's worth reading the getting started guide linked above, as well as the guide on migrating from Enzyme.

import * as React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button', () => {
  it('can trigger a function by being clicked', () => {
    const onClick = jest.fn();
    const { getByRole } = render(<Button onClick={onClick}>This is a button</Button>);

    userEvent.click(getByRole('button'));
    expect(onClick).toHaveBeenCalled();
  });
});

Tips for interaction testing

When testing interactions, generally use @testing-library/user-event instead of fireEvent to simulate the full sequence of events triggered by user interaction in the browser. (See also considerations for fireEvent.)

If you're having trouble getting an interaction test working, see My event isn't working properly below.

Snapshot testing

🚨 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 exists
  • expect.toMatchInlineSnapshot() saves the snapshot within the test file itself
    • Prefer inline snapshots: they're more obvious to read and harder to ignore. If the snapshot seems too long to save in the file, that's often a good reason to consider ways to test the desired outcome directly.

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:

  • for v8: yarn update-snapshots
  • for v9: yarn test -u

FAQ

See also Tips for interaction testing above.

Browser methods aren't working

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).

My event isn't working properly

There are a few possibilities here:

  • Make sure to use @testing-library/user-event to more realistically simulate the sequence of user events that would occur in a real interaction.
  • Try using fake timers and manually advancing them to ensure the event and related updates are triggered.
  • In some cases, jsdom does not provide realistic event simulation (this is especially an issue for tests involving focus), so try testing the scenario with Cypress instead.

getByRole can't find an element or says it's hidden

testing-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:

  • A few components have an initial off-screen render for measurement purposes (example: v8 CommandBar). In this case, use fake timers and advance the timers past the initial render before running other tests.
  • Maybe the component (or the part you're trying to test) is actually not accessible--try it with a screen reader to check
  • Some rare edge case where testing-library is wrong (or you want to use 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 })

Clone this wiki locally