| name | tdesign-test-component |
| description | This skill should be used when writing or maintaining unit tests for TDesign React components. It provides testing patterns, best practices, and SOP for Vitest-based component testing including snapshot tests. |
TDesign 组件测试指南
本技能提供 TDesign React 组件单元测试的完整 SOP 和最佳实践。
触发条件
当用户需要执行以下任务时使用此技能:
- 为组件编写单元测试
- 更新或修复测试用例
- 运行快照测试
- 提高测试覆盖率
测试框架和工具
| 工具 | 用途 |
|---|
| Vitest | 测试运行器 |
| @testing-library/react | React 组件测试工具 |
| @testing-library/user-event | 用户交互模拟 |
| @testing-library/jest-dom | DOM 断言扩展 |
测试文件规范
文件位置和命名
packages/components/[component-name]/
└── __tests__/
├── vitest-[component-name].test.jsx # 单元测试
└── __snapshots__/ # 快照文件(自动生成)
命名格式:vitest-[component-name].test.jsx
⚠️ 注意:测试文件由 API 平台生成,文件头部有注释标记。
测试工具导入
import { render, fireEvent, vi } from '@test/utils';
import { ComponentName } from '..';
@test/utils 已封装常用工具,包括:
render - 组件渲染
fireEvent - 事件触发
vi - Vitest mock 工具
act - React act wrapper
userEvent - 用户交互模拟
mockDelay - 异步延迟模拟
simulateInputChange - 输入模拟
simulateKeydownEvent - 键盘事件模拟
SOP 流程
Step 1: 基础渲染测试
测试组件能否正常渲染:
describe('ExampleComponent', () => {
it('renders correctly', () => {
const { container } = render(<ExampleComponent />);
expect(container.firstChild).toHaveClass('t-example-component');
});
});
Step 2: Props 测试
为每个 prop 编写测试用例:
it('props.disabled works fine', () => {
const { container: container1 } = render(<Button>Text</Button>);
expect(container1.querySelector('.t-is-disabled')).toBeFalsy();
const { container: container2 } = render(<Button disabled={true}>Text</Button>);
expect(container2.firstChild).toHaveClass('t-is-disabled');
expect(container2).toMatchSnapshot();
const { container: container3 } = render(<Button disabled={false}>Text</Button>);
expect(container3.querySelector('.t-is-disabled')).toBeFalsy();
});
['small', 'medium', 'large'].forEach((item) => {
it(`props.size is equal to ${item}`, () => {
const { container } = render(<Button size={item}>Text</Button>);
if (item !== 'medium') {
expect(container.firstChild).toHaveClass(`t-size-${item[0]}`);
}
expect(container).toMatchSnapshot();
});
});
Step 3: TNode 渲染测试
测试自定义渲染内容:
it('props.children works fine', () => {
const { container } = render(
<Button>
<span className="custom-node">TNode</span>
</Button>
);
expect(container.querySelector('.custom-node')).toBeTruthy();
expect(container).toMatchSnapshot();
});
it('props.icon works fine', () => {
const { container } = render(
<Button icon={<span className="custom-icon">Icon</span>}>Text</Button>
);
expect(container.querySelector('.custom-icon')).toBeTruthy();
});
Step 4: 事件测试
测试组件事件回调:
it('events.click works fine', () => {
const fn = vi.fn();
const { container } = render(<Button onClick={fn}>Click</Button>);
fireEvent.click(container.firstChild);
expect(fn).toHaveBeenCalled();
expect(fn.mock.calls[0][0].type).toBe('click');
});
it('disabled button should not trigger click', () => {
const fn = vi.fn();
const { container } = render(<Button disabled onClick={fn}>Click</Button>);
fireEvent.click(container.firstChild);
expect(fn).not.toHaveBeenCalled();
});
Step 5: 异步操作测试
测试异步行为:
import { mockDelay } from '@test/utils';
it('async operation works fine', async () => {
const { container } = render(<AsyncComponent />);
fireEvent.click(container.querySelector('.trigger'));
await mockDelay(300);
expect(container.querySelector('.result')).toBeTruthy();
});
Step 6: 用户交互测试
使用 userEvent 进行复杂交互测试:
import { userEvent } from '@test/utils';
it('user input works fine', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const { container } = render(<Input onChange={onChange} />);
const input = container.querySelector('input');
await user.type(input, 'hello');
expect(onChange).toHaveBeenCalled();
expect(input.value).toBe('hello');
});
快照测试
CSR 快照测试
位置:test/snap/csr.test.jsx
自动为所有 _example/*.tsx 生成快照:
NODE_ENV=test-snap pnpm test
SSR 快照测试
位置:test/snap/ssr.test.jsx
测试服务端渲染输出。
更新快照
pnpm test -- --update
pnpm test button -- --update
运行测试命令
pnpm test
pnpm test button
pnpm test -- --coverage
NODE_ENV=test-snap pnpm test
pnpm test -- --ui
常见测试模式
测试 Loading 状态
it('loading state shows loading indicator', () => {
const { container } = render(<Button loading>Text</Button>);
expect(container.firstChild).toHaveClass('t-is-loading');
expect(container.querySelector('.t-loading')).toBeTruthy();
});
测试 DOM 属性
it('href attribute works fine', () => {
const { container } = render(<Button href="https://example.com">Link</Button>);
expect(container.firstChild.getAttribute('href')).toBe('https://example.com');
expect(container.firstChild.tagName.toLowerCase()).toBe('a');
});
测试 className 组合
it('custom className is merged', () => {
const { container } = render(<Button className="custom-class">Text</Button>);
expect(container.firstChild).toHaveClass('t-button');
expect(container.firstChild).toHaveClass('custom-class');
});
Mock ResizeObserver
class ResizeObserver {
observe() { return this; }
unobserve() { return this; }
}
beforeAll(() => {
global.ResizeObserver = ResizeObserver;
});
Mock 时间
import MockDate from 'mockdate';
beforeEach(() => {
MockDate.set('2020-12-28 00:00:00');
});
afterEach(() => {
MockDate.reset();
});
测试检查清单
编写测试时,确保覆盖:
测试覆盖率要求
- 整体覆盖率目标:> 80%
- 新组件要求:> 90% 行覆盖率
- 核心逻辑:100% 分支覆盖
常见问题
Q: 测试文件是否可以手动编辑?
A: vitest-*.test.jsx 文件由 API 平台生成,文件头有注释标记。可以在其基础上添加额外测试,但核心测试应通过 API 平台维护。
Q: 如何处理异步组件?
A: 使用 mockDelay 或 act wrapper,确保异步操作完成后再进行断言。
Q: 快照测试失败怎么办?
A: 首先检查变更是否预期内。如果是预期变更,使用 --update 参数更新快照。