Unit tests are required to write component libraries in React. There are two common test libraries in React: Enzyme and React-testing-library.

In terms of downloads, react-testing-library is higher, and react-testing-library is more compatible with React V17. Therefore, this article uses react-testing-library.

This paper is divided into two parts.

  1. The first part is installation and configuration.
  2. The second part provides examples of common use cases.

The test framework for this article is Jest.

Jest installation

The test library needs to run on the test framework, so you need to install Jest first.

Install the Jest

npm install --save-dev jest
Copy the code

By default, Jest can only use CommonJS to import and export, so you need to install Babel so that Jest supports ESM syntax and can parse JSX syntax.

Install Babel

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react
Copy the code

Create the Babel configuration file babel.config.js in the root directory.

module.exports = {
  presets: [["@babel/preset-env", { targets: { node: "current"}}]."@babel/preset-react",]};Copy the code

@babel/preset-react because Babel is needed to parse JSX syntax, if you want to write test cases in TypeScript, install @babel/preset- TypeScript.

Adding test files

The test file should contain.test.js by default, so we create a test file named index.test.js in the tests directory.

// tests/index.test.js
import React from "react";

describe("test".() = > {
  test("equal".() = > {
    expect(<div />).toEqual(<div />);
  });
});

Copy the code

Write a random test case.

Run the jest command.

jest
Copy the code

The jest command tests all files that match the rules.

jest tests/index.test.js
Copy the code

You can also specify a file.

Jest brief tutorial

If you don’t know how to use Jest, here’s a quick overview of what you can do.

describe

Describe code used to contain a piece of test, usually to group several tests, or to nest multiple layers of tests on its own,

describe("test".() = > {
  // x x x
  describe("group".() = > {
  	// x x x
  });
});
Copy the code

test

Test is each test that contains the method to be tested. Its alias function is IT, and they are equivalent.

test("test".() = > {
  expect(1).toBe(1)});Copy the code

It can be written in describe or outside.

expect

Expect, translated as expected, needs toBe used in conjunction with many matchers, such as the toBe matcher above.

expect(1).toBe(1)
Copy the code

This test is expressed in language as the expectation that the value of 1 should be 1. ToBe defaults to direct comparison, and toEqual is used to determine whether objects are equal or not.

toBeCalled

The toBeCalled use case matches whether the function is called, which is usually used to test the event passed into the component.

test("onClick".() = > {
  const fn = jest.fn()
  const btn = <button onClick={fn}>button</button>
  / / click BTN
  expect(fn).toBeCalled()
});
Copy the code

The test function passed in needs to be created using jest.fn().

ToBeTruthy and toBeFalsy

Like the name, it is used to determine whether the value is true or false.

not

We can use not if we want to test for “the value of 2 is not 1”.

expect(2).not.toBe(1)
Copy the code

Once you have a basic concept, you can write code directly and learn more matchers as you write.

Install React Testing Library

Although its name is React Testing Library, its package is called @testing-library/ React.

npm install --save-dev @testing-library/react @testing-library/jest-dom
Copy the code

Testing-library /jest-dom adds some additional matchers to test the DOM.

You need to add it to the jEST configuration and set the JEST environment to JsDOM.

Create the jest configuration file jest.config.js in the root directory.

module.exports = {
  setupFilesAfterEnv: ["@testing-library/jest-dom"].testEnvironment: "jsdom"};Copy the code

Now our test environment can test the React code.

import React from "react";
import { render } from "@testing-library/react";

test("component".() = > {
  const { getByLabelText } = render(<button aria-label="Button" />);
  expect(getByLabelText("Button")).toBeEmptyDOMElement();
});
Copy the code

Try running the jest command ~

Use @testing-library/react to use @testing-library/react.

I’ll share common test cases with you.

Common test cases

A common test case is to simulate the user’s actions and determine if they meet the expected results.

Test <button/> click

Usually we can’t judge whether the button is clicked, so we can judge by simulating whether the button click event is called after the user clicks.

import React from "react";
import { render, fireEvent } from "@testing-library/react";

test("component".() = > {
  const onClick = jest.fn(); // Test the function
  // render is used to render elements
  const { getByLabelText } = render(
    <button aria-label="Button" onClick={onClick} />
  );
  // getByLabelText can get the element from the value of aria-label
  const btn = getByLabelText("Button");
  fireEvent.click(btn); // Simulate the click event
  expect(onClick).toBeCalled(); // Expect to be called
  expect(onClick).toBeCalledTimes(1); // Expect to be called once
});
Copy the code

Tests the value and input of <input/>

We need to simulate the change event to change the value of the input box.

test("Input box input, check value".() = > {
  const onChange = jest.fn();
  const { getByTestId } = render(
    <input data-testid="input" onChange={onChange} />
  );
  // To retrieve an element by means of the data-testid
  const input = getByTestId("input");
  // Simulates the change event, the second parameter emulates the value of event
  fireEvent.change(input, { target: { value: "test"}}); expect(onChange).toBeCalled(); expect(input).toHaveValue("test");
});
Copy the code

Tests whether the element is disabled and contains a class name

ToBeDisabled is used to match disable. Sometimes we manually control the focus value of the element and append the corresponding class name to the value. ToHaveClass can be used to match the inclusion of the class name.

test("Test if the element is disabled, contains a certain class name.".() = > {
  const { getByText } = render(
    <button disabled className="button-disabled">
      this is a button
    </button>
  );
  // getByText gets elements from text
  const btn = getByText("this is a button");
  expect(btn).toBeDisabled();
  expect(btn).toHaveClass("button-disabled");
});
Copy the code

Test whether the props change takes effect on the element

In @testing-library/ React you need to use rerender to change props, toHaveTextContent to match textContent.

test("Test whether the props change works on the element".() = > {
  const Demo = ({ loading }) = > (
    <button aria-label="Button">{loading ? "loading" : "button"}</button>
  );
  const { getByLabelText, rerender } = render(<Demo />);
  const btn = getByLabelText("Button");
  expect(btn).toHaveTextContent("button");
  // Rerender to simulate the props change
  rerender(<Demo loading />);
  expect(btn).toHaveTextContent("loading");
});
Copy the code

Tests whether the child element contains a class name

Sometimes a class name is added to a child element by a controlled value. You can use getElementsByClassName to get a child element by its class name. You can use getElementsByClassName to determine if the drop-down box is open to the class name, or if the list has a class name for the selected element.

test("Tests whether child elements contain a certain class name.".() = > {
  const Demo = ({ loading }) = > (
    <button aria-label="Button">
      <span className={loading ? "loading" : "button"} >button</span>
    </button>
  );
  const { baseElement } = render(<Demo loading />);
  const ele = baseElement.getElementsByClassName("loading");
  expect(ele.length).toBe(1);
});
Copy the code

Test asynchronous invocation events

If the event is contained in an asynchronous method, you need to test it using waitFor.

The toBeCalledWith match is used to test whether the event call is passed the appropriate parameters.

test("Test asynchronous events".async() = > {const Demo = ({ onClick }) = > {
    const asyncClick = async() = > {await Promise.resolve();
      onClick("click");
    };
    return <button onClick={asyncClick}>button</button>;
  };
  const fn = jest.fn();
  const { getByText } = render(<Demo onClick={fn} />);
  const btn = getByText("button");
  fireEvent.click(btn);
  await waitFor(() = > expect(fn).toBeCalledWith("click"));
});
Copy the code

Test timer

If you include a timer in your code, you can use jEST’s corresponding API to fast forward time.

In addition to runAllTimers, runOnlyPendingTimers are commonly used.

test("Test timer".() = > {
  jest.useFakeTimers(); / / use fakeTimer
  const Demo = ({ onClick }) = > {
    const waitClick = () = > {
      setTimeout(() = > {
        onClick();
      }, 10000);
    };
    return <button onClick={waitClick}>button</button>;
  };
  const fn = jest.fn();
  const { getByText } = render(<Demo onClick={fn} />);
  const btn = getByText("button");
  fireEvent.click(btn);
  jest.runAllTimers(); // Execute all timers
  expect(fn).toBeCalled();
  jest.useRealTimers(); / / use realTimer
});
Copy the code

The snapshot of the test

Snapshot can directly compare components to see if they have changed. If a component has changed, it does not match Snapshot. After determining that the component has no problems, you need to run jest -u to update snapshot.

test(The snapshot "tests".() = > {
  const Demo = () = > (
    <form>
      <input name="test" type="text" />
      <button type="submit">submit</button>
    </form>
  );
  const { asFragment } = render(<Demo />);
  expect(asFragment()).toMatchSnapshot();
});
Copy the code

Hooks in the test

The tests for hooks require another package to be installed.

npm install --save-dev @testing-library/react-hooks
Copy the code

Basic testing

import React from "react";
import { render, waitFor } from "@testing-library/react";
import { renderHook } from "@testing-library/react-hooks";

test("Hooks" test.() = > {
  const useCounter = () = > {
    const [count, setCount] = React.useState(0);
    const increment = React.useCallback(() = > setCount((x) = > x + 1), []);
    return { count, increment };
  };
  const { result } = renderHook(() = > useCounter());
  // result.current contains the return value of hooks
  expect(result.current.count).toBe(0);
  // result.current.increment() calls need to be placed in waitFor
  waitFor(() = > result.current.increment());
  expect(result.current.count).toBe(1);
});
Copy the code

Asynchronous Hooks test

The asynchronous hooks test provides a wait function, waitForNextUpdate, which waits 1000 milliseconds by default and can be changed by passing parameters.

test("Asynchronous Hooks tests".async() = > {const useCounter = () = > {
    const [count, setCount] = React.useState(0);
    const incrementAsync = React.useCallback(
      () = > setTimeout(() = > setCount((x) = > x + 1), 100), []);return { count, incrementAsync };
  };

  const { result, waitForNextUpdate } = renderHook(() = > useCounter());
  result.current.incrementAsync();
  // waitForNextUpdate waits for the next update, 1000 milliseconds by default
  await waitForNextUpdate();
  expect(result.current.count).toBe(1);
});
Copy the code

Let’s start with a couple of common use cases, code repositories.