• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

当你写完一个 React 组件,如何保证它的功能是正常的呢?

在浏览器里渲染出来,手动测试一遍就好了啊。

那如果这个组件交给别人维护了,他并不知道这个组件的功能应该是什么样的,怎么保证他改动代码之后,组件功能依然正常?

这种情况就需要单元测试了。

单元测试可以测试函数、类的方法等细粒度的代码单元,保证功能正常。

有了单元测试之后,后续代码改动只需要跑一遍单元测试就知道功能是否正常。

但很多同学觉得单元测试没意义,因为代码改动比较频繁,单元测试也跟着需要频繁改动。

确实,如果代码改动特别频繁,就没必要单测了,手动测试就好。

因为如果手动测试一遍需要 5 分钟,写单元测试可能需要一个小时。

但如果代码比较稳定,那单测还是很有必要的,比如组件库里的组件、hooks 库里的 hooks、一些工具函数等。

手动测试 5 分钟,每次都要手动测试,假设 20 次,那就是 100 分钟的成本,而且还不能保证测试是可靠的。

写单测要一个小时,每次直接跑单测自动化测试,跑 100 次也是一个小时的成本,而且还是测试结果很可靠。

综上,单元测试能保证函数、类的方法等代码单元的功能正常,把手动测试变成自动化测试。

但是写单元测试成本还是挺高的,如果代码改动频繁,那手动测试更合适。一些比较稳定的代码,还是有必要写单测的,写一次,自动测试 n 次,收益很大。

那 React 的组件和 hooks 怎么写单测呢?

这节我们一起来写几个单测试试。

用 create-react-app 创建个 react 项目:

npx create-react-app --template=typescript react-unit-test

测试 react 组件和 hooks 可以使用 @testing-library/react 这个包,然后测试用例使用 jest 来组织。

这两个包 cra 都给引入了,我们直接跑下 npm run test 就可以看到单测结果。

App 组件是这样的:

它的单测是这么写的:

通过 @testing-library/react 的 render 函数把组件渲染出来。

通过 screen 来查询 dom,查找文本内容匹配正则 /learn react/ 的 a 标签。

然后断言它在 document 内。

你也可以这么写:

test("renders learn react link 2", () => {
    const { container } = render(<App />);
    const linkElement = container.querySelector(".App-link");

    expect(linkElement?.textContent).toMatch(/learn react/i);
});

render 会返回组件挂载的容器 dom,它是一个 HTMLElement 的对象,有各种 dom 方法。

可以用 querySelector 查找到那个 a 标签,然后判断它的内容是否匹配正则。

这两种写法都可以。

第二种方法更容易理解,就是拿到渲染容器的 dom,再用 dom api 来查找 dom。

第一种方法的 screen 是 @testing-library/react 提供的 api,是从全局查找 dom,可以直接根据文本查(getByText),根据标签名和属性查(getByRole) 等。

antd 组件的测试也是用的第二种来查找 dom 的:

那如果有 onClick、onChange 等事件监听器的组件,怎么测试呢?

我们写个组件 Toggle.tsx:

import { useCallback, useState } from "react";

function Toggle() {
    const [status, setStatus] = useState(false);

    const clickHandler = useCallback(() => {
        setStatus((prevStatus) => !prevStatus);
    }, []);

    return (
        <div>
            <button onClick={clickHandler}>切换</button>
            <p>{status ? "open" : "close"}</p>
        </div>
    );
}

export default Toggle;

有个 state 来存储 open、close 的状态,点击按钮切换。

渲染出来是这样的:

这个组件如何测试呢?

单测里触发事件需要用到 fireEvent 方法了。

改下 App.test.tsx

import { render, fireEvent } from "@testing-library/react";
import Toggle from "./Toggle";

test("toggle", () => {
    const { container } = render(<Toggle />);

    expect(container.querySelector("p")?.textContent).toBe("close");

    fireEvent.click(container.querySelector("button")!);

    expect(container.querySelector("p")?.textContent).toBe("open");
});

用 render 方法把组件渲染出来。

用 container 节点的 dom api 查询 p 标签的文本,断言是 close。

然后用 fireEvent.click 触发 button 的点击事件。

断言 p 标签的文本是 open。

跑一下:

npm run test

测试通过了:

fireEvent 可以触发任何元素的任何事件:

那如何触发 change 事件呢?

这样写:

第二个参数传入 target 的 value 值。

此外,如果我有段异步逻辑,过段时间才会渲染内容,这时候怎么测呢?

比如 Toggle 组件里点击按钮之后,过了 2s 才改状态:

setTimeout(() => {
    setStatus((prevStatus) => !prevStatus);
}, 2000);

这时候测试用例就报错了:

这种用 waitFor 包裹下,设置 timeout 的时间就好了:

await waitFor(
    () => expect(container.querySelector("p")?.textContent).toBe("open"),
    {
        timeout: 3000,
    }
);

测试通过了:

除了这些之外,还有一个 api 比较常用,就是 act

它是 react-dom 包里的,@testing-library/react 对它做了一层包装。

就是可以把所有浏览器里跑的代码都包一层 act,这样行为会和在浏览器里一样。

文档里的例子是这样的:

把单测里的 fireEvent 用 act 包一层:

import { render, fireEvent, waitFor } from "@testing-library/react";
import { act } from "react-dom/test-utils";
import Toggle from "./Toggle";

test("toggle", async () => {
    const { container } = render(<Toggle />);
    expect(container.querySelector("p")?.textContent).toBe("close");

    act(() => {
        fireEvent.click(container.querySelector("button")!);
    });

    await waitFor(
        () => expect(container.querySelector("p")?.textContent).toBe("open"),
        {
            timeout: 3000,
        }
    );
});

结果一样:

组件测试我们学会了,那如果我想单独测试 hooks 呢?

这就要用到 renderHook 的 api 了。

我们写个 useCounter 的 hook:

import { useState } from "react";

type UseCounterReturnType = [
    count: number,
    increment: (delta: number) => void,
    decrement: (delta: number) => void,
];

export default function useCounter(
    initialCount: number = 0
): UseCounterReturnType {
    const [count, setCount] = useState(initialCount);

    const increment = (delta: number) => {
        setCount((count) => count + delta);
    };

    const decrement = (delta: number) => {
        setCount((count) => count - delta);
    };

    return [count, increment, decrement];
}

先在 App.tsx 里用一下:

import useCounter from "./useCounter";

function App() {
    const [count, increment, decrement] = useCounter();

    return (
        <div>
            <div>{count}</div>
            <div>
                <button onClick={() => increment(1)}>加一</button>
                <button onClick={() => decrement(2)}>减二</button>
            </div>
        </div>
    );
}

export default App;

跑一下:

npm run start

没啥问题。

然后来写下这个 hook 的单测:

test("useCounter", async () => {
    const hook = renderHook(() => useCounter(0));

    const [count, increment, decrement] = hook.result.current;

    act(() => {
        increment(2);
    });
    expect(hook.result.current[0]).toBe(2);

    act(() => {
        decrement(3);
    });
    expect(hook.result.current[0]).toBe(-1);

    hook.unmount();
});

renderHook 返回的 result.current 就是 hook 的返回值。

这就是 hook 的单测写法。

案例代码上传了小册仓库。

总结

单元测试能保证函数、类的方法等代码单元的功能正常,把手动测试变成自动化测试。

变更不频繁的代码,还是有必要写单测的,写一次,自动测试 n 次,收益很大。

我们学了 react 组件和 hook 的单测写法。

主要是用 @testing-library/react 这个库,它有一些 api:

  • render:渲染组件,返回 container 容器 dom 和其他的查询 api
  • fireEvent:触发某个元素的某个事件
  • createEvent:创建某个事件(一般不用这样创建)
  • waitFor:等待异步操作完成再断言,可以指定 timeout
  • act:包裹的代码会更接近浏览器里运行的方式
  • renderHook:执行 hook,可以通过 result.current 拿到 hook 返回值

其实也没多少东西。

jest 的 api 加上 @testing-libary/react 的这些 api,就可以写任何组件、hook 的单元测试了。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
10.快速掌握Storybook
Next
12.深入理解Suspense和ErrorBoundary