Тестирование компонентов на практике

Тестирование компонентов — хорошее начало для демонстрации практического кода тестирования. Компонентные тесты более содержательны, чем простые модульные тесты, менее сложны, чем сквозное тестирование, и демонстрируют взаимодействие с DOM. С философской точки зрения, использование React позволило веб-разработчикам думать о веб-сайтах или веб-приложениях как о состоящих из компонентов.

Таким образом, тестирование отдельных компонентов, независимо от того, насколько они сложны, — хороший способ начать думать о тестировании нового или существующего приложения.

На этой странице описывается тестирование небольшого компонента со сложными внешними зависимостями. Компонент, который не взаимодействует с каким-либо другим кодом, легко протестировать, например, нажав кнопку и подтвердив увеличение числа. На самом деле такого кода очень мало, и код тестирования, в котором нет взаимодействий, может иметь ограниченную ценность.

Тестируемый компонент

Мы используем Vitest и его среду JSDOM для тестирования компонента React. Это позволяет нам быстро запускать тесты с помощью Node в командной строке, эмулируя браузер.

Список имен с кнопкой «Выбрать» рядом с каждым именем.
Небольшой компонент React, который показывает список пользователей из сети.

Этот компонент React с именем UserList извлекает список пользователей из сети и позволяет вам выбрать одного из них. Список пользователей получается с помощью fetch внутри useEffect , а обработчик выбора передается через Context . Это его код:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

Этот пример не демонстрирует лучшие практики React (например, он использует fetch внутри useEffect ), но ваша кодовая база, скорее всего, будет содержать много подобных случаев. Более того, на первый взгляд эти случаи могут показаться сложными для тестирования. В следующем разделе этого курса будет подробно рассмотрено написание тестируемого кода.

Вот что мы тестируем в этом примере:

  • Убедитесь, что в ответ на данные из сети создается правильный DOM.
  • Убедитесь, что щелчок по пользователю вызывает обратный вызов.

Каждый компонент отличается. Что делает тестирование этого интересным?

  • Он использует глобальную fetch для запроса реальных данных из сети, которые при тестировании могут быть нестабильными или медленными.
  • Он импортирует другой класс, UserRow , который мы, возможно, не захотим неявно тестировать.
  • Он использует Context , который не является частью тестируемого кода и обычно предоставляется родительским компонентом.

Напишите быстрый тест для начала

Мы можем быстро протестировать что-то очень простое в этом компоненте. Чтобы внести ясность, этот пример не очень полезен. Но полезно настроить шаблон в одноранговом файле с именем UserList.test.tsx (помните, что средства запуска тестов, такие как Vitest, по умолчанию запускают файлы, заканчивающиеся на .test.js или аналогичные, включая .tsx ):

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

Этот тест утверждает, что при рендеринге компонента он содержит текст «Пользователи». Это работает , хотя побочным эффектом компонента является отправка fetch в сеть. В конце теста fetch все еще выполняется, без установленной конечной точки. Мы не можем подтвердить, что какая-либо информация о пользователе отображается после завершения теста, по крайней мере, без ожидания тайм-аута.

Ложная fetch()

Мокинг — это замена реальной функции или класса чем-то под вашим контролем для теста. Это обычная практика практически во всех типах тестов, за исключением самых простых модульных тестов. Подробнее это описано в разделе «Утверждения и другие примитивы» .

Вы можете имитировать fetch() для своего теста, чтобы он быстро завершался и возвращал ожидаемые данные, а не «реальные» или неизвестные данные. fetch является глобальным , что означает, что нам не нужно import или require его в нашем коде.

В vitest вы можете создать макет глобального объекта, вызвав vi.stubGlobal со специальным объектом, возвращаемым vi.fn() — при этом создается макет, который мы можем изменить позже. Эти методы рассматриваются более подробно в следующем разделе этого курса, но вы можете увидеть их на практике в следующем коде:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

Этот код добавляет макет, описывает поддельную версию Response на выборку по сети, а затем ожидает ее появления. Если текст не появляется (вы можете проверить это, изменив запрос в queryByText на новое имя), тест завершится неудачно.

В этом примере использовались встроенные помощники по макетированию Vitest, но другие среды тестирования имеют аналогичные подходы к макетированию. Уникальность Vitest заключается в том, что после всех тестов необходимо вызвать vi.unstubAllGlobals() или установить эквивалентную глобальную опцию . Без «отмены» нашей работы макет fetch может повлиять на другие тесты, и на каждый запрос будет отправлена ​​наша странная куча JSON.

Ложный импорт

Возможно, вы заметили, что наш компонент UserList сам импортирует компонент под названием UserRow . Хотя мы не включили его код, вы можете видеть, что он отображает имя пользователя: предыдущий тест проверяет наличие «Sam», и оно не отображается непосредственно внутри UserList , поэтому оно должно быть получено из UserRow .

Блок-схема того, как имена пользователей перемещаются через наш компонент.
UserListTest не видит UserRow .

Однако UserRow сам по себе может быть сложным компонентом — он может извлекать дополнительные пользовательские данные или иметь побочные эффекты, не относящиеся к нашему тесту. Удаление этой изменчивости сделает ваши тесты более полезными, особенно если компоненты, которые вы хотите протестировать, становятся более сложными и более переплетенными со своими зависимостями.

К счастью, вы можете использовать Vitest для макетирования определенных импортируемых объектов, даже если ваш тест не использует их напрямую, так что любой код, который их использует, имеет простую или известную версию:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

Подобно издевательству над глобальной fetch , это мощный инструмент, но он может стать неустойчивым, если в вашем коде много зависимостей. Опять же, лучшее решение — написать тестируемый код.

Нажмите и укажите контекст

В React и других библиотеках, таких как Lit , есть концепция под названием Context . Пример кода включает UserContext , который вызывает метод, если выбран пользователь. Это часто рассматривается как альтернатива «сверлению реквизита», когда обратный вызов передается непосредственно в UserList .

Наша тестовая программа не предоставила UserContext . Добавление действия щелчка в тест React без него может в худшем случае привести к сбою теста. В лучшем случае, если экземпляр по умолчанию был предоставлен где-то еще, это может привести к некоторому поведению, находящемуся вне нашего контроля (аналогично неизвестному UserRow выше).

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

Вместо этого при рендеринге компонента вы можете предоставить свой собственный Context . В этом примере используется экземпляр vi.fn() , фиктивной функции Vitest , которую можно использовать для проверки того, был ли выполнен вызов и какие аргументы он использовал.

В нашем случае это взаимодействует с ложной fetch в ​​предыдущем примере, и тест может подтвердить, что переданный идентификатор был sam :

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

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

В итоге

В этом примере показано, как создать тест компонента для тестирования и защиты сложного в тестировании компонента React. Этот тест был направлен на то, чтобы убедиться, что компонент правильно взаимодействует со своими зависимостями: глобальным fetch , импортированным подкомпонентом и Context .

Проверьте свое понимание

Какие подходы использовались для тестирования компонента React?

Имитация сложных зависимостей с помощью простых для тестирования
Внедрение зависимостей с использованием контекста
Заглушка глобальных переменных
Проверка того, что число увеличилось