上一篇文章探讨了如何使用 Vitest 和 React 测试库将 React Hooks 作为独立单元进行测试。在这篇文章中,我们将继续学习如何以可维护和可扩展的方式利用 React 组件进行单元测试。
先决条件
您应该设置并运行一个 React 项目。推荐的方法是使用命令 来初始化您的项目,并使用 Vite 作为其捆绑管理工具npm create vite@latest
。
为了进行测试,我们需要安装以下依赖项:
为此,我们运行以下命令:
npm install -D vitest jsdom @testing-library/react
#OR
yarn add -D vitest jsdom @testing-library/react
在vitest.config.js
(或vite.config.js
对于 Vite 项目)中,我们添加以下test
对象:
//...
export default defineConfig({
test: {
global: true,
environment: 'jsdom',
},
})
我们还在文件中添加了一个新test:unit
命令package.json
来运行单元测试,如下所示:
"scripts": {
"test:unit": "vitest --root src/",
}
接下来,我们将进行额外的设置以使 Vitest 断言 DOM 元素。
扩展Vitest的expect方法
Vitest 提供了用于expect
断言值的基本断言方法。但是,它没有 DOM 元素的断言方法,例如toBeInTheDocument()
或toHaveTextContent()
。对于此类方法,我们可以安装该@testing-library/jest-dom
包并扩展expect
Vitest 中的方法以包含该包中的断言方法matchers
。
为此,我们将setupTest.js
在项目的根目录中创建一个文件并添加以下代码:
/**setupTest.js */
import { expect } from 'vitest';
import matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);
在 中vitest.config.js
,我们可以将setupTest.js
文件添加到test.setupFiles
字段中:
//vitest.config.js
/**... */
test: {
/**... */
setupFiles: './setupTest.js',
},
/**... */
通过此设置,expect()
现在将拥有测试 React 组件所需的所有 DOM 断言方法。
让我们看看如何使用 Vitest 和 React 测试库来测试 React 组件。
使用模拟测试电影组件
在本节中,我们将研究一个简单的组件 –Movies
它显示具有以下功能的电影列表:
- 该组件从外部源获取电影列表。
- 用户可以按标题搜索电影。
下面的屏幕截图显示了该组件在 UI 上的外观:
该组件的示例实现Movies
如下:
export const Movies = () => {
const { movies } = useMovies();
const {searchTerm, setSearchTerm, filteredItems: filteredMovies} = useSearch(movies);
return (
<section>
<div>
<label htmlFor="search">Search</label>
<input
type="search" id="search" value={searchTerm}
data-testid="search-input-field"
onChange={event => setSearchTerm(event.target.value)}
/>
</div>
<ul data-testid="movies-list">
{filteredMovies.map((movie, index) => (
<li key={index}>
<article>
<h2>{movie.title}</h2>
<p>Release on: {movie.release_date}</p>
<p>Directed by: {movie.director}</p>
<p>{movie.opening_crawl}</p>
</article>
</li>
))}
</ul>
</section>
)
};
我们将从Vitest 中的方法(作为Vitest 实例)监视和模拟useMovies
和useSearch
钩子开始,如以下代码所示:vi.spyOn()``vi
import * as useMoviesHooks from '../hooks/useMovies';
import * as useSearchHooks from '../hooks/useSearch';
describe('Movies', () => {
const useMoviesSpy = vi.spyOn(useMoviesHooks, 'useMovies');
const useSearchSpy = vi.spyOn(useSearchHooks, 'useSearch');
});
我们将使用方法模拟它们的返回值mockReturnValue
,如下所示:
describe('Movies', () => {
/**... */
it('should render the app', () => {
const items = [{
title: 'Star Wars',
release_date: '1977-05-25',
director: 'George Lucas',
opening_crawl: 'It is a period of civil war.'
}];
useMoviesSpy.mockReturnValue({
movies: items,
});
useSearchSpy.mockReturnValue({
searchTerm: '',
setSearchTerm: vi.fn(),
filteredItems: items
});
/**... */
});
})
然后,我们将Movies
使用render
from 的方法渲染组件@testing-library/react
,并断言该组件按预期渲染电影列表,如下所示:
import { describe, it, expect, vi } from 'vitest';
import { Movies } from './Movies';
import { render } from '@testing-library/react';
describe('Movies', () => {
/**... */
it('should render the the list of movies', () => {
/**... */
const { getByTestId } = render(<Movies />);
expect(
getByTestId('movies-list').children.length
).toBe(items.length);
});
})
该方法将检索属性值等于的getByTestId
元素,然后我们可以断言其子元素等于模拟数组的长度。data-testid``movies-list``items
使用data-testid
属性值是一种很好的做法,可以识别 DOM 元素以进行测试,并避免影响组件在生产和测试中的实现。
接下来,我们将测试搜索钩子在Movies
.
测试搜索输入的功能
我们首先仅模拟useMovies
返回一组电影的钩子,如下所示:
it('should change the filtered items when the search term changes', () => {
const items = [
{ title: 'Star Wars' },
{ title: 'Star Trek' },
{ title: 'Starship Troopers' }
];
useMoviesSpy.mockReturnValue({
movies: items,
isLoading: false,
error: null
});
});
我们渲染Movies
组件并使用getByTestId
以下方法检索搜索输入字段data-testid
:
it('should change the filtered items when the search term changes', () => {
/**... */
const { getByTestId } = render(<Movies />);
const searchInput = getByTestId('search-input-field');
});
为了测试 UI 中的搜索功能Movies
,我们将使用以下两种方法@testing-library/react
:
fireEvent.change()
– 模拟change
搜索输入字段上的用户事件。act()
– 环绕用户事件模拟的执行,并确保在继续对 UI 上显示的项目数量进行断言之前,所有更新都应用于 DOM。
import { fireEvent, render, act } from '@testing-library/react';
it('should change the filtered items when the search term changes', () => {
/**... */
act(() => {
fireEvent.change(searchInput, { target: { value: 'Wars' } });
})
expect(
getByTestId('movies-list').children.length
).toBe(1);
});
通过此,我们测试了用户与搜索输入字段的交互以及组件对用户输入的响应。
但是,如果您在上一个测试之后运行该测试,则该测试将失败,因为 的最后一个模拟值useSearch
仍然有效。我们必须在每次测试后清理并恢复原始实现,以确保每个测试用例的模拟值都是隔离的。我们将在下一节中这样做。
每次测试后clear模拟
为了清除我们监视的每个钩子的任何模拟值,我们将触发mockClear()
如下:
afterEach(() => {
useMoviesSpy.mockClear();
useSearchSpy.mockClear();
});
使用此代码,每次测试运行后,Vitest 将清除任何现有的模拟值或间谍挂钩的实现,为下一次测试运行做好准备。或者,我们可以使用它mockRestore()
来恢复非模拟实现。
接下来,我们将应用类似的方法,为每个测试运行(但所有测试套件)清理 DOM。
每次测试后clear DOM
在 中setupTest.js
,我们可以在每次测试后运行cleanup
from 的方法@testing-library/react
来清理 DOM,使用afterEach
Vitest 中的方法,如下所示:
/**setupTest.js */
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
/**... */
afterEach(() => {
cleanup();
});
通过这样做,我们可以确保 DOM 在每次测试运行之前都是干净的,并将其应用于所有测试套件。
概括
本文向我们展示了如何使用 React 测试库和 Vitest 包以及正确的模拟方法和适当的测试方法来测试 React 组件。
我们可以将示例测试的测试扩展到涵盖更多场景,例如在加载电影时测试加载状态或错误状态,或者为搜索输入添加更多过滤选项。通过正确的组件和钩子结构,我们可以以有组织且可扩展的方式创建我们的测试系统。