本文介绍了如何使用最先进的的 Vitest 结合 react-testing-library 测试 React 组件库,以及创造性的引入 documentation test 概念来测试我们的 mdx demo 代码。
公众号:
JavaScript与编程艺术
🧪 单元测试
单元测试简称单测。我们采用 Vitest + react-testing-library (RTL) 做为测试框架。目录结构如下:
tests
setup.mjs # 测试初始化脚本
src
ComponentA
index.tsx
index.test.tsx # 建议和源文件放一起,好处是让新加入的成员能快速感知到 TDD 意识
...
🖥️ 环境准备
安装依赖:
npm install vitest jsdom @testing-library/react @testing-library/jest-dom --save-dev
如果你的 node.js 版本低于 18,只能安装 vitest@0.33.0
新增文件 tests/setup.mjs,写入以下内容:
// tests/setup.js
// @ts-check
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from "@testing-library/jest-dom/matchers";
expect.extend(matchers); // 对 expect 的能力增强,不用可注释
// 清屏:解决单个文件内多个 test 多次 render,后面的 render 会累积前面 render 产生的 DOM 节点问题
afterEach(() => {
cleanup();
});
// 如果遇到 window.matchMedia undefined is not a function 可以开启
// window.matchMedia = vi.fn().mockImplementation((query) => ({
// matches: false,
// media: query,
// onchange: null,
// addListener: vi.fn(),
// removeListener: vi.fn(),
// }))
新增 vitest.config.mjs 写入以下内容:
// @ts-check
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
// @ts-expect-error
plugins: [react()],
test: {
setupFiles: './tests/setup.mjs',
environment: 'jsdom',
coverage: {
thresholds: {
branches: 20, // 自行设置合理值
functions: 20,
lines: 20,
},
include: ['src/'], // 只计算 src 内文件覆盖率
},
},
resolve: {
alias: [
{
find: '@',
replacement: '/src', // 如果有设置 tsconfig.json paths 比如 `@`
},
{
find: 'name-in-package.json', // 组件名,package.json 的 name。目的是文档测试
replacement: '/src',
},
],
},
})
更新 package.json,新增以下 script:
"test": "vitest",
"ci": "vitest run --coverage",
test:本地写单测会用到,将 watch 单测和配置文件达到热更新的效果。ci:ci 流程会用到或在发布前进行自动化测试,此处会读取 vitest.config.mjs 中设置的 coverage 阈值,如果低于阈值 ci 将失败。
📝 书写单测
下面结合例子来说明如何写一个单测。
比如有如下组件,我们想测试是否能正常展示 Hello React。
// src/App.tsx
import * as React from 'react';
const title = 'Hello React';
function App() {
return <div>{title}</div>;
}
export default App;
新增测试文件 src/App.test.tsx:
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
it('renders App component', () => {
render(<App />);
expect(screen.getByText('Hello React')).toBeDefined();
});
});
再比如我们有一个登录组件,我们想测试在没有输入任何内容时,点击登录按钮会出现提示。
expect(screen.queryByText('Please Enter Username / Email.')).toBeNull()
expect(screen.queryByText('Please Enter Password.')).toBeNull()
fireEvent.click(screen.getByRole('button'))
expect(screen.queryByText('Please Enter Username / Email.')).toBeDefined()
expect(screen.queryByText('Please Enter Password.')).toBeDefined()
注意我们此处用的是
queryByText,因为getByText找不到文本将报错。
更多示例以及关于 get|queryByText 等 API 使用和选择可以参考 www.robinwieruch.de/react-testi…。
此处不再详述。
📝✅ 文档测试
如果我们能对 demo 进行单测,那该多好。相当于对用户契约有了自动化保障,类比 Rust 的 Documentation Comments as Tests。
Nothing is better than documentation with examples. But nothing is worse than examples that don't work because the code has changed since the documentation was written.
没有什么比带有示例的文档更好了。但是也没有什么比文档中的示例由于代码更新而无法运行更糟糕的了。
它强调了示例对于文档的重要性,同时指出如果文档中的示例因为代码变化而不能用,那是非常糟糕的情况。这就好比你买了一本电器使用说明书,上面有操作示范,但如果因为产品更新,这些示范都不对了,那会让人很头疼。对于开发者来说,文档中的示例应该保持与当前代码的一致性和有效性。
文档中的示例可以帮助理解,但需要确保示例的代码与文档一致,传统做法是定期检查代码与文档的匹配度。我们是否还有更好的手段?那就是文档测试。
针对 dumi 而言,文档测试是指我们在 markdown 中写的示例。还记得我们在 vitest.config.mjs 中配置的 alias 吗?
{
find: 'name-in-package.json', // 你的 UI 组件库名字,即 package.json 的 name。目的是无缝进行文档测试
replacement: '/src',
},
该配置是让示例代码中 import { ComponentA } from 'name-in-package.json' 能正确解析的关键。
举例说明,若我们有如下文档:
// index.md
<code src="./demo/app.tsx"></code>
demo 内容为:
// demo/app.tsx
import React from 'react'
import { ComponentA } from 'name-in-package.json'
export default () => {
return <ComponentA ... />
}
在同目录 demo/ 下新增测试文件 demo/app.test.tsx:
// demo/app.test.tsx
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import Demo from './demo';
describe('Demo', () => {
it('renders Demo component', () => {
render(<Demo />);
expect(screen.getByText('Hello React')).toBeDefined();
});
});
至此我们已完成对一个组件的单测和文档测试,可以在该组件的 index.md 标题添加单测通过的 tag!
index.md:
- # ComponentA / 中文标题
+ # ComponentA / 中文标题 <Badge type="success">test passing</Badge>
当然也可以增加类似 npm 的 badge:
[](https://github.com/umijs/dumi)
效果: