React Testing Library Installation and basic usage This article digs deeper into some of the more complex scenarios.

The mock test

Before we start RTL testing, let’s review Jest’s Mock tests a bit.

A mock test is a way to continue testing progress by substituting a dummy object for an object that is not easy to construct or obtain.

There are two most common mocks:

  1. Create a mock function and inject it into the target use case as followsjest.fn
  2. Design a manual mock object to override dependent modules such asjest.mock

jest.fn()

Start with mock function injection. Let’s write a very basic repeatTen function that calls other functions 10 times.

function repeatTen(fn) {
  for (let i = 0; i < 10; i++) {
    fn();
  }
}

repeatTen(() = > console.log("Onion"));
Copy the code

The characteristic of repeatTen function is that it doesn’t care about the specific implementation of FN, as long as fn can run 10 times at a time. This type of unit test is perfect for using mock functions. As shown below, we pass a mock function (onChang) to repeatTen as an argument and check the status of the mock function to see if the function is running as expected.

it("Count the calls of the onChange".() = > {
  const onChange = jest.fn();
  repeatTen(onChange);
  // assert that it is called 10 times
  expect(onChange).toHaveBeenCalledTimes(10);
});
Copy the code

jest.mock()

The implementation of repeatTen function above is simple, but the real world is much more complicated. Some functions rely on tripartite libraries for implementation. In a test environment it is very difficult to pass in a mock function to simulate the actual running state. For example, the following function calls the API through AXIos to return data, so think about how to write a test case.

const loadStories = () = > axios.get("/stories");
Copy the code

To test loadStories without the possibility of calling a real API, we had to play around with the internal axios module — using jest. Mock (” Axios “) to automatically emulate the AXIos module.

Jest provides an interesting dependency override method — Jest. Mock (“axios”), which provides a mockResolvedValue to the.get method of the object module — axios. In plain English, let axios.get(“/stories”) return a false response; And the data of this response is prepared in advance.

Take a look at the main flow for writing mock dependency tests:

  1. usingjest.mock(...)Override the dependency of import inside the use case function
  2. Construct a mock return for one of the methods used inside the use case function
  3. Asserts the return result of the use case function
import axios from "axios";

// step1: override a module
jest.mock("axios");

const mockStories = [
  { objectID: "1".title: "Hello" },
  { objectID: "2".title: "React"},]; it("load stories by axios".async() = > {// step2: make a fake response
  const response = { data: mockStories };
  axios.get.mockResolvedValue(response);

  // step3: assert the data
  const { data } = await loadStories();
  expect(data).toEqual(mockStories);
});
Copy the code

The callback test

OK, it goes around and around a lot about the Jest method. Let’s return to the React Testing Library.

If you’ve read the Jest. Fn section, RTL callback tests are used to test onChange events. Let’s write a simple search bar that returns the input of the parent component via the onChange event.

// CallbackSearch.js
export function CallbackSearch({ onChange }) {
  return (
    <div>
      <label htmlFor="search">Search:</label>
      <input id="search" type="text" onChange={onChange} />
    </div>
  );
}
Copy the code

How do I write the tests for this component? It’s easy, there’s only one onChange argument anyway, and we just need to test its call status.

import userEvent from "@testing-library/user-event";

describe("Search".() = > {
  it("paste counts".() = > {
    const onChange = jest.fn();
    render(<CallbackSearch onChange={onChange} />);

    const $e = screen.getByRole("textbox");
    userEvent.paste($e, "Onion");

    expect(onChange).toHaveBeenCalledTimes(1);
  });
});
Copy the code

This time we use the userEvent paste method, which will only trigger the event once, so declare that onChange was called once. If you use userEvent.type, which is called once for every character entered, you should assert that onChange is used as many times as the length of the input string.

Asynchronous load test

The jest. Fn case was covered in the previous chapter, so let’s talk about the jest. Mock case.

React Hook

Isn’t it popular to write Hook now? Let’s write an enhanced Hook for loadStories. The main function is the same as before, using AXIos to call the API remotely. On success, we write data to the Stories state, and on failure, we write errors to the Error state.

// userStory.js
import axios from "axios";
import { useState, useCallback } from "react";

export function useStory() {
  const [stories, setStories] = useState([]);
  const [error, setError] = useState(null);

  const handleFetch = useCallback(() = > {
    const loadStories = async() = > {try {
        const { data } = await axios.get("/stories");
        setStories(data);
      } catch(error) { setError(error); }}; loadStories(); } []);return { error, stories, handleFetch };
}
Copy the code

To write unit tests for React Hook, install an additional dependency, @testing-library/react-hooks; It uses one of its methods, readerHook, to simulate the React component.

Let’s see how to write this Hook test case. The formula is similar:

  1. usingjest.mockOverwrite the AXIos module
  2. Forge aaxios.getThe response of the
  3. Asserts that the initial state of stories is an empty array
  4. Call an asynchronous method to load stories
  5. After completing the asynchronous call, assert the latest status of Stories
import { renderHook } from "@testing-library/react-hooks";
import axios from "axios";
import { useStory } from "./useStory";

// Step 1: override a module
jest.mock("axios");

const mockStory = [
  { objectID: "1".title: "Hello" },
  { objectID: "2".title: "React"},]; it("load stories and succeed".async() = > {// Step 2: fake a response
  const response = { data: mockStory };
  axios.get.mockResolvedValue(response);

  const { result, waitForNextUpdate } = renderHook(() = > useStory());

  // Step 3: test initial state
  expect(result.current.stories).toEqual([]);

  // Step 4: fetch stories
  result.current.handleFetch();

  // Step 5: test after load axios api
  await waitForNextUpdate();
  expect(result.current.stories).toEqual(mockStory);
});
Copy the code

React Component

After testing the Hook, we put it into the component. Old rule, don’t look at the implementation, look at the effect:

Basically, you write a button, and when clicked, it asynchronously invokes the data and displays all the stories entries.

Or do the same as above:

  1. usingjest.mockOverwrite the AXIos module
  2. Forge aaxios.getThe response of the
  3. Click the button to load Stories asynchronously
  4. When finished, the assertion screen will have the corresponding story entry
import axios from "axios";

// Step 1: override a module
jest.mock("axios");

const mockStory = [
  { objectID: "1".title: "Hello" },
  { objectID: "2".title: "React"},]; describe("FetchButton".() = > {
  it("fetches stories from an API and displays them".async () => {
    render(<FetchButton />);

    // Step 2: fake a response
    const response = { data: mockStory };
    axios.get.mockResolvedValue(response);

    // Step 3: click button
    userEvent.click(screen.getByRole("button"));

    // Step 4: assert that screen will display 2 items
    const $items = await screen.findAllByRole("listitem");
    expect($items).toHaveLength(2);
  });
});
Copy the code

The TDD in the teaching book requires that you write the test first, and then write the implementation. I’m going to do my best to do the layout, and after you’ve seen the unit tests, you should have a general component implementation outline, right? Let me write my implementation:

// FetchButton.js
import { useStory } from "./useStory";

export function FetchButton() {
  const { error, stories, handleFetch } = useStory();

  return (
    <div>
      <button onClick={handleFetch}>Fetch Stories</button>

      {error && <span>Something went wrong ...</span>}

      <ul>
        {stories.map((story) => (
          <li key={story.objectID}>{story.title}</li>
        ))}
      </ul>
    </div>
  );
}
Copy the code

Abnormal test

Let’s go back to the FetchButton above. While writing the implementation, I discovered a major omission in the previous tests: asynchronous call apis could return errors, and the previous unit tests didn’t cover throwing exceptions. So I added an error judgment to the implementation; Something went wrong… . Since there are omissions, I will continue to make up the test case. This test is actually simpler and basically repeats the steps above, with the only difference being that axios.get returns mockRejectedValue. Look at the following test case, there should be no difficulty.

jest.mock("axios");

describe("FetchButton", () => {

  it("fetch stories from an API but fail", async () => {

    render(<FetchButton />);

+ axios.get.mockRejectedValue(new Error()));

    userEvent.click(screen.getByRole("button"));

    const $errorMsg = await screen.findByText(/Something went wrong/);

    expect($errorMsg).toBeInTheDocument();
  });
});
Copy the code

summary

I recently looked back at the 2020 JS Satisfaction survey and found that Testing Library topped the list, which is very popular. We spent two sessions introducing the Testing Library introductory tutorial, and we hope that you can quickly implement TDD into your own projects. I’ve seen projects rot in a year or two with almost no testing; As soon as a new function was added, there would be leaks everywhere, and the number of bugs increased rapidly in the later period, so release could even be delayed for half a year. When shit piles up, you can’t change it. TDD has been the best practice in the software industry for many years, and we, as practitioners in this industry, should also act firmly according to the industry rules. This is not only “work hard” so simple, more is the embodiment of their professional quality, mutual encouragement.

related

  • React Testing Library 101 (1)