Тестирование пользовательских перехватчиков реакции, использующих выборку (или другое асинхронное поведение)

Пользовательский крючок

Есть несколько ошибок при тестировании пользовательских хуков с асинхронным поведением, таких как API выборки. На момент написания вы должны использовать альфа-версию (v16.9.0-alpha.0) react, react-dom и react-test-renderer. Надеюсь, что в недалеком будущем это станет частью стабильной кодовой базы React.

Обновление: версия 16.9.0 стала стабильной, не используйте альфа-версию, как упомянуто выше.

Если вы получаете такие ошибки, как…

Warning: An update to TestHook inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
        in TestHook
        in Suspense

… Это может помочь.

Вот простой пример написанного мной пользовательского хука, который вызывает API с использованием fetch:

Основная ссылка

import { useEffect, useState, useRef } from "react";

export const useDataApi = () => {
  const firstUpdate = useRef(true);
  const [error, setError] = useState(false);
  const [apiUrl, setApiUrl] = useState("");
  const [data, setData] = useState(null);

  useEffect(
    () => {
      if (!firstUpdate.current) {
        fetch(apiUrl)
          .then(response => response.json())
          .then(response => {
            setData(response);
          })
          .catch(error => {
            setError(true);
          });
      }
      firstUpdate.current = false;
    },
    [apiUrl]
  );
  return { data, error, callApi: setApiUrl };
};

Я использую useRef(true), чтобы убедиться, что он не выполняет вызов при первом рендеринге, и useState для обновления состояния ошибки / данных, которое затем возвращается для использования другими компонентами. useEffect следит за тем, чтобы вызов API запускался только тогда, когда apiUrl был обновлен через setApiUrl.

Его можно импортировать и использовать в другом компоненте, вызвав callApi со строкой, например:

...
const { data, error, callApi } = useDataApi();
...
function handleClick(type, filterValue) {   
  callApi("your/data/endpoint/here.com");
}
...
return(  
  ...
  {data.whateverIsReturned}
  ...
)

Вы также можете использовать переменные error для условного отображения сообщений об ошибках.

Тестирование

В моих примерах используется шутка, но вам понадобится еще несколько библиотек, чтобы заставить это работать.

Boilerplate

Ваш тестовый файл без каких-либо тестов должен начинаться так:

Основная ссылка

import React from "react";
import { useDataApi } from "path/to/hoo/useDataApi.jsx";
import "whatwg-fetch";
import { renderHook } from "@testing-library/react-hooks";
import fetchMock from "fetch-mock";
import { act } from "react-test-renderer";

describe("useDataApi", () => {
  beforeAll(() => {
    global.fetch = fetch;
  });
  afterAll(() => {
    fetchMock.restore();
  });
});

В наших beforeAll и afterAll мы устанавливаем global.fetch на полифилированную версию fetch, чтобы ее можно было использовать в ваших шутливых тестах, а затем мы восстанавливаем это после завершения тестов.

Тесты

Следует запомнить несколько вещей.

  • Используйте act, чтобы обернуть любое поведение, которое обновляет состояние поведения, в нашем случае это вызывает callApi, но это также может быть, например, нажатие кнопки.
  • Поскольку выборка является асинхронной, мы должны использовать await act(async () => {...})

Проверка успешного ответа

Основная ссылка

import React from "react";
import { useDataApi } from "path/to/hoo/useDataApi.jsx";
import "whatwg-fetch";
import { renderHook } from "@testing-library/react-hooks";
import fetchMock from "fetch-mock";
import { act } from "react-test-renderer";

describe("useDataApi", () => {
  beforeAll(() => {
    global.fetch = fetch;
  });
  afterAll(() => {
    fetchMock.restore();
  });

  it("should return data with a successful request", async () => {
    const { result } = renderHook(() => useDataApi());
    fetchMock.mock("test.com", {
      returnedData: "foo"
    });
    await act(async () => {
      result.current.callApi("test.com");
    });

    expect(result.current.data).toBe({
      returnedData: "foo"
    });
  });
});

Проверка ошибочного ответа

Основная ссылка

import React from "react";
import { useDataApi } from "path/to/hoo/useDataApi.jsx";
import "whatwg-fetch";
import { renderHook } from "@testing-library/react-hooks";
import fetchMock from "fetch-mock";
import { act } from "react-test-renderer";

describe("useDataApi", () => {
  beforeAll(() => {
    global.fetch = fetch;
  });
  afterAll(() => {
    fetchMock.restore();
  });

  it("should return error as true if api error", async () => {
    const { result } = renderHook(() => useDataApi());

    fetchMock.mock("test.com", 500);

    await act(async () => {
      result.current.callApi("test.com");
    });

    expect(result.current.data).toBe(null);
    expect(result.current.error).toBe(true);
  });
});

Все Гисты можно найти здесь.