使用 Vitest 高效测试您的 React hooks

在这篇文章中,我们将探讨如何使用 Vitest 和 React 测试库来帮助对 React hooks 进行单元测试,从而使它们易于维护和扩展。

设置 Vitest 和 JSDOM

Vitest由 Vite 提供支持,声称是 Vite 项目的“极速单元测试框架”。Vitest 提供与 Jest 类似的功能和语法,开箱即用地支持 TypeScript/JSX,同时观看 (HRM) 和运行测试速度更快。

尽管最初是为 Vite 支持的项目构建的,但我们也可以在非 Vite 项目(例如 Webpack)中通过适当的配置使用 Vitest。

要在 React 项目中设置 Vitest,我们可以使用以下命令将 Vitest 安装为开发依赖项

yarn -D vitest

我们还安装jsdom(或任何 DOM 标准实现)作为开发依赖项:

yarn add -D jsdom

然后在vitest.config.js(或vite.config.js对于 Vite 项目)中,我们将以下test对象添加到配置中,作为jsdom测试的 DOM 环境:

//...
export default defineConfig({
  test: {
    environment: 'jsdom',
  },
})

我们还可以进行设置,global: true以便不必在每个测试文件中显式导入包中的每个方法,例如expectdescribeitvi实例。vitest

完成后vitest.config.js,我们将一个新命令添加到scriptsinpackage.json文件中以运行单元测试,如下所示:

"scripts": {
    "test:unit": "vitest --root src/"
}

在上面的代码中,我们将测试的根目录设置为src/文件夹。或者,我们可以将其放在田地vite.config.js下面test.root。两种方法的工作原理相同。

接下来,让我们添加一些测试。

使用 React 测试库测试常规钩子

Vitest 支持测试任何 JavaScript 和 TypeScript 代码。但是,为了测试 React 组件特定的功能(例如 React hooks),我们仍然需要为所需的 hook 创建一个包装器并模拟 hook 的执行。

为此,我们可以安装并使用React 测试库中的 Render hooks

yarn add -D @testing-library/react-hooks

完成后,我们可以使用renderHook此包中的方法来呈现所需的钩子。renderHook将返回一个对象实例,其中包含result属性和其他有用的实例方法,例如unmountwaitFor。然后我们可以从属性访问钩子的返回值result.current

例如,让我们看一下一个useSearch钩子,它接收初始项目数组并返回反应式 searchTerm 的对象、过滤后的项目列表以及更新搜索词的方法。其实现如下:

//hooks/useSearch.ts
import { useState, useMemo } from "react";


export const useSearch = (items: any[]) => {
    const [searchTerm, setSearchTerm] = useState('');
    const filteredItems = useMemo(
            () => items.filter(
                movie => movie.title.toLowerCase().includes(searchTerm.toLowerCase())
            )
        , [items, searchTerm]);

    return {
        searchTerm,
        setSearchTerm,
        filteredItems
    };
}

我们可以编写一个测试来检查钩子 forsearchTerm和 for的默认返回值filterItems,如下所示:

import { expect, it, describe } from "vitest";
import { renderHook } from '@testing-library/react-hooks'
import { useSearch } from "./useSearch"

describe('useSearch', () => { 
    it('should return a default search term and original items', () => { 
        const items = [{ title: 'Star Wars' }];



        const { result } = renderHook(() => useSearch(items));

        expect(result.current.searchTerm).toBe('');
        expect(result.current.filteredItems).toEqual(items);
    });
});

为了测试更新时钩子是否起作用searchTerm,我们可以使用该act()方法来模拟setSearchTerm执行,如下测试用例所示:

import { /**... */ act } from "vitest";




//...
    it('should return a filtered list of items', () => { 
        const items = [            { title: 'Star Wars' },            { title: 'Starship Troopers' }        ];




        const { result } = renderHook(() => useSearch(items));



        act(() => {
            result.current.setSearchTerm('Wars');
        });


        expect(result.current.searchTerm).toBe('Wars');
        expect(result.current.filteredItems).toEqual([{ title: 'Star Wars' }]);
    });
//...

请注意,您不能破坏result.current实例的反应性属性,否则它们将失去反应性。例如,下面的代码将不起作用:

const { searchTerm } = result.current;




act(() => {
    result.current.setSearchTerm('Wars');
});





expect(searchTerm).toBe('Wars'); // This won't work

接下来,我们可以继续测试useMovies包含异步逻辑的更复杂的钩子。

使用异步逻辑测试钩子

让我们看一下下面的钩子实现示例useMovies

export const useMovies = ():{ movies: Movie[], isLoading: boolean, error: any } => {
    const [movies, setMovies] = useState([]);


    const fetchMovies = async () => {
        try {
            setIsLoading(true);
            const response = await fetch("https://swapi.dev/api/films");



            if (!response.ok) {
                throw new Error("Failed to fetch movies");
            }


            const data = await response.json();
            setMovies(data.results);
        } catch (err) {
        //do something
        } finally {
        //do something
        }
    };

    useEffect(() => {
        fetchMovies();
    }, []);

    return { movies }
}

在上面的代码中,钩子fetchMovies使用同步效果钩子在第一个渲染上运行异步调用useEffect。当我们尝试测试钩子时,此实现会导致问题,因为renderHookfrom 的方法@testing-library/react-hooks不会等待异步调用完成。由于我们不知道提取何时会解决,因此我们无法movies在完成后断言该值。

为了解决这个问题,我们可以使用waitForfrom 的方法@testing-library/react-hooks,如以下代码所示:

/**useMovies.test.ts */
describe('useMovies', () => {
    //...
    it('should fetch movies', async () => {
        const { result, waitFor } = renderHook(() => useMovies());




        await waitFor(() => {
            expect(result.current.movies).toEqual([{ title: 'Star Wars' }]);
        });
    });
    //...
});

waitFor接受回调并返回一个 Promise,该 Promise 在回调成功执行时解析。在上面的代码中,我们等待该movies值等于预期值。或者,我们可以传递一个对象作为第二个参数来waitFor配置轮询的超时和间隔。例如,我们可以将超时设置为 1000ms,如下所示:

await waitFor(() => {
    expect(result.current.movies).toEqual([{ title: 'Star Wars' }]);
}, {
    timeout: 1000
});

这样,如果movies1000 毫秒后该值不等于预期值,则测试将失败。

使用spyOn和waitFor监视和测试外部API调用

在之前的测试中useMovies,我们使用API​​获取外部数据fetch,这对于单元测试来说并不理想。相反,我们应该使用该vi.spyOn方法(作为viVitest 实例)来监视该global.fetch方法并模拟其实现以返回虚假响应,如以下代码所示:

import { /**... */ vi, beforeAll } from "vitest";




describe('useMovies', () => {

    //Spy on the global fetch function
    const fetchSpy = vi.spyOn(global, 'fetch');




    //Run before all the tests
    beforeAll(() => {
        //Mock the return value of the global fetch function
        const mockResolveValue = { 
            ok: true,
            json: () => new Promise((resolve) => resolve({ 
                results: [{ title: 'Star Wars' }] 
            }))
        };

        fetchSpy.mockReturnValue(mockResolveValue as any);
    });

    it('should fetch movies', async () => { /**... */ }
});

fetchSpy在上面的代码中,我们用我们创建的值模拟了使用其方法的返回值mockReturnValue()。通过这种实现,我们可以在不触发真正的 API 调用的情况下运行测试,从而减少由于外部因素导致测试失败的机会。

由于我们模拟了该fetch方法的返回值,因此我们需要在测试完成后使用mockRestoreVitest 中的方法恢复其原始实现,如以下代码所示:

import { /**... */ vi, beforeAll, afterAll } from "vitest";




describe('useMovies', () => {

    const fetchSpy = vi.spyOn(global, 'fetch');

    /**... */

    //Run after all the tests
    afterAll(() => {
        fetchSpy.mockRestore();
    });
});

另外,我们还可以使用该mockClear()方法清除所有mock的信息,例如调用次数和mock的结果。当断言模拟的调用或模拟具有不同测试的不同返回值的同一函数时,此方法非常方便。我们通常使用mockClear()inbeforeEachafterEach方法来确保我们的测试完全隔离。

就是这样。您现在可以继续有效地测试您的自定义挂钩。

笔记

不幸的是,目前@testing-library/react-hooks不适用于 React 18。该包正在迁移到 React 测试库的官方包 ( @testing-library/react) 中。某些功能,例如waitForNextUpdate将不再可用。

概括

在本文中,我们将尝试如何使用 React Hooks 测试库和 Vitest 包来测试自定义钩子。我们还学习了如何使用该方法测试具有异步逻辑的钩子,以及如何使用Vitest 的方法waitFor监视外部 API 调用。vi.spyOn,所以 在下一篇文章中我将介绍——如何使用 React 测试库和 Vitest 包测试 React 组件

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

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

昵称

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