Тестирование компонентов — хорошее начало для демонстрации практического кода тестирования. Компонентные тесты более содержательны, чем простые модульные тесты, менее сложны, чем сквозное тестирование, и демонстрируют взаимодействие с DOM. С философской точки зрения, использование React позволило веб-разработчикам думать о веб-сайтах или веб-приложениях как о состоящих из компонентов.
Таким образом, тестирование отдельных компонентов, независимо от того, насколько они сложны, — хороший способ начать думать о тестировании нового или существующего приложения.
На этой странице описывается тестирование небольшого компонента со сложными внешними зависимостями. Компонент, который не взаимодействует с каким-либо другим кодом, легко протестировать, например, нажав кнопку и подтвердив увеличение числа. На самом деле такого кода очень мало, и код тестирования, в котором нет взаимодействий, может иметь ограниченную ценность.
Тестируемый компонент
Мы используем Vitest и его среду JSDOM для тестирования компонента React. Это позволяет нам быстро запускать тесты с помощью Node в командной строке, эмулируя браузер.
Этот компонент 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
.
Однако 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?