From 9482f10ea9d3e1a1169d0cecf7522e19f006025a Mon Sep 17 00:00:00 2001 From: liuchuanzong Date: Fri, 31 May 2024 14:36:58 +0800 Subject: [PATCH] docs: add missing testing doc --- docs/guide/auto-api-table.md | 3 +- docs/guide/mobile-library.md | 4 +- docs/guide/page-tab.md | 2 +- docs/guide/test.md | 227 +++++++++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 docs/guide/test.md diff --git a/docs/guide/auto-api-table.md b/docs/guide/auto-api-table.md index e610cd54d..053cbf241 100644 --- a/docs/guide/auto-api-table.md +++ b/docs/guide/auto-api-table.md @@ -1,7 +1,7 @@ --- title: 自动 API 表格 group: 进阶 -order: 2 +order: 4 --- # 自动 API 表格实验性 @@ -12,6 +12,7 @@ order: 2 - API 表格仅支持展示解析出的顶层属性 - `@description` 不支持多语言配置 - Windows 操作系统上会工作不正常,目前不建议使用 + ::: dumi 支持基于 JSDoc 及 TypeScript 类型定义自动为 React 组件生成 API 表格,降低 API 文档的维护成本。 diff --git a/docs/guide/mobile-library.md b/docs/guide/mobile-library.md index 8868dda1b..c3536f519 100644 --- a/docs/guide/mobile-library.md +++ b/docs/guide/mobile-library.md @@ -1,6 +1,6 @@ --- group: 进阶 -order: 2 +order: 3 --- # 移动端组件研发 @@ -10,7 +10,7 @@ order: 2 与 dumi 1 一样,只需要安装移动端组件研发主题即可切换到移动端组件研发模式: ```bash -$ npm i dumi-theme-mobile@^2.0.0 -D +npm i dumi-theme-mobile@^2.0.0 -D ``` 然后可以通过 `themeConfig` 配置该主题包的行为: diff --git a/docs/guide/page-tab.md b/docs/guide/page-tab.md index 01ecbfd10..8a37e285b 100644 --- a/docs/guide/page-tab.md +++ b/docs/guide/page-tab.md @@ -1,6 +1,6 @@ --- group: 进阶 -order: 1 +order: 2 --- # 页面 Tab diff --git a/docs/guide/test.md b/docs/guide/test.md new file mode 100644 index 000000000..7a0dc3e15 --- /dev/null +++ b/docs/guide/test.md @@ -0,0 +1,227 @@ +--- +title: 单元测试 +group: 进阶 +order: 1 +--- + +## 单元测试 + +单元测试简称单测。采用 Vitest + react-testing-library (RTL) 做为我们的测试框架。目录结构如下: + +```text +tests + setup.mjs # 测试初始化脚本 +src + ComponentA + index.tsx + index.test.tsx # 建议和源文件放一起,好处是让新加入的成员能快速感知到 TDD 意识 +... +``` + +### 环境准备 + +安装依赖: + +```sh +npm install vitest jsdom @testing-library/react @testing-library/jest-dom --save-dev +``` + +新增文件 tests/setup.mjs,写入以下内容: + +```js +// 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 写入以下内容: + +```js +// @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: + +```json +"test": "vitest", +"ci": "vitest run --coverage", +``` + +- `test`:本地写单测会用到,将 watch 单测和配置文件达到热更新的效果。 +- `ci`:ci 流程会用到或在发布前进行自动化测试,此处会读取 vitest.config.mjs 中设置的 coverage 阈值,如果低于阈值 ci 将失败。 + +### 书写单测 + +下面结合例子来说明如何写一个单测。 + +比如有如下组件,我们想测试是否能正常展示 `Hello React`。 + +```tsx +// src/App.tsx +import * as React from 'react'; + +const title = 'Hello React'; + +function App() { + return
{title}
; +} + +export default App; +``` + +新增测试文件 `src/App.test.tsx`: + +```tsx {9} +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; + +import App from './App'; + +describe('App', () => { + it('renders App component', () => { + render(); + + expect(screen.getByText('Hello React')).toBeDefined(); + }); +}); +``` + +再比如我们有一个登录组件,我们想测试在没有输入任何内容时,点击登录按钮会出现提示。 + +```tsx +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 使用和选择可以参考 。 +此处不再详述。 + +#### 文档测试 + +如果我们能对 demo 进行单测,那该多好。相当于对用户契约有了自动化保障,类比 Rust 的 documentation test。 +> 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 吗? + +```js +{ + find: 'name-in-package.json', // 组件名,package.json 的 name。目的是文档测试 + replacement: '/src', +}, +``` + +该配置是让示例代码中 `import { ComponentA } from 'name-in-package.json'` 能正确解析的关键。 +举例说明,若我们有如下文档: + +```md +// index.md + +``` + +demo 内容为: + +```tsx +// demo/app.tsx +import React from 'react' +import { ComponentA } from 'name-in-package.json' + +export default () => { + return +} +``` + +在同目录 demo/ 下新增测试文件 demo/app.`test`.tsx: + +```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(); + + expect(screen.getByText('Hello React')).toBeDefined(); + }); +}); +``` + +至此我们已完成对一个组件的**单测**和**文档测试**,可以在该组件的 index.md 标题添加单测通过的 tag! + +index.md: + +```diff +- # ComponentA / 中文标题 ++ # ComponentA / 中文标题 test passing +``` + +当然也可以增加类似 npm 的 badge: + +```text +[![dumi](https://img.shields.io/badge/test%20-passing-green)](https://github.com/umijs/dumi) +```