使用 Vitest 高效地进行组件测试

上一篇文章探讨了如何使用 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包并扩展expectVitest 中的方法以包含该包中的断言方法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 实例)监视和模拟useMoviesuseSearch钩子开始,如以下代码所示: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使用renderfrom 的方法渲染组件@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,我们可以在每次测试后运行cleanupfrom 的方法@testing-library/react来清理 DOM,使用afterEachVitest 中的方法,如下所示:

/**setupTest.js */

import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';




/**... */

afterEach(() => {
  cleanup();
});

通过这样做,我们可以确保 DOM 在每次测试运行之前都是干净的,并将其应用于所有测试套件。

概括

本文向我们展示了如何使用 React 测试库和 Vitest 包以及正确的模拟方法和适当的测试方法来测试 React 组件。

我们可以将示例测试的测试扩展到涵盖更多场景,例如在加载电影时测试加载状态或错误状态,或者为搜索输入添加更多过滤选项。通过正确的组件和钩子结构,我们可以以有组织且可扩展的方式创建我们的测试系统。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYbPi27t' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片