单元测试
本文档介绍 vue3-element-admin 的单元测试配置、使用方法和最佳实践。
测试框架
| 依赖 | 说明 |
|---|---|
| Vitest | 基于 Vite 的快速单元测试框架 |
| @vue/test-utils | Vue 组件测试工具 |
| happy-dom | 轻量级 DOM 环境(比 jsdom 快) |
| @testing-library/vue | Vue Testing Library |
快速开始
安装依赖
bash
pnpm add -D vitest @vitest/ui @vue/test-utils happy-dom @testing-library/vue @testing-library/user-event运行测试
bash
# 监听模式
pnpm test
# 单次运行
pnpm test:run
# 生成覆盖率报告
pnpm test:coverage
# 打开测试 UI
pnpm test:ui目录结构
tests/
├── unit/
│ ├── utils/ # 工具函数测试
│ ├── store/ # Store 模块测试
│ ├── composables/ # 组合式函数测试
│ └── components/ # 组件测试
└── setup.ts # 测试环境配置测试覆盖
已完成
| 模块 | 文件 | 覆盖内容 |
|---|---|---|
| utils | storage.test.ts | localStorage/sessionStorage 操作、批量清理 |
| utils | validate.test.ts | URL、邮箱、手机号验证 |
| utils | format.test.ts | 增长率、文件大小、数字格式化 |
| utils | auth.test.ts | Token 管理、权限判断 |
| store | dict.test.ts | 字典缓存、防重复请求 |
| store | app.test.ts | 侧边栏、设备、语言管理 |
| store | settings.test.ts | 主题、布局、特殊模式 |
| composables | useTableSelection.test.ts | 表格选择管理 |
| composables | useDictSync.test.ts | 字典同步 |
| components | Pagination.test.ts | 分页组件 |
待添加
user.test.ts- 用户 Storepermission.test.ts- 权限 StoreDictSelect.test.ts- 字典选择组件DictTag.test.ts- 字典标签组件
编写指南
测试原则
FIRST 原则:
- Fast(快速)- 测试应该快速运行
- Independent(独立)- 测试之间不应相互依赖
- Repeatable(可重复)- 测试结果应该一致
- Self-Validating(自我验证)- 测试应该自动判断通过或失败
- Timely(及时)- 测试应该与代码同步编写
AAA 模式:
typescript
it("应该做某事", () => {
// Arrange - 准备
const input = "test";
// Act - 执行
const result = someFunction(input);
// Assert - 断言
expect(result).toBe("expected");
});工具函数测试
typescript
import { describe, it, expect } from "vitest";
import { formatFileSize } from "@/utils/format";
describe("formatFileSize", () => {
it("应该格式化字节为 KB", () => {
expect(formatFileSize(1024)).toBe("1.00 KB");
});
it("应该处理 0 字节", () => {
expect(formatFileSize(0)).toBe("0 B");
});
});Store 模块测试
typescript
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setActivePinia, createPinia } from "pinia";
import { useDictStore } from "@/store/modules/dict";
vi.mock("@/api/system/dict", () => ({
default: { getDictItems: vi.fn() },
}));
describe("useDictStore", () => {
beforeEach(() => {
setActivePinia(createPinia());
localStorage.clear();
vi.clearAllMocks();
});
it("应该缓存字典数据", async () => {
const store = useDictStore();
// ...
});
});组件测试
typescript
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Pagination from "@/components/Pagination/index.vue";
describe("Pagination", () => {
it("应该正确渲染", () => {
const wrapper = mount(Pagination, {
props: { page: 1, limit: 10, total: 100 },
});
expect(wrapper.exists()).toBe(true);
});
it("应该触发 pagination 事件", async () => {
const wrapper = mount(Pagination, {
props: { page: 1, limit: 10, total: 100 },
});
await wrapper.vm.handleCurrentChange(2);
expect(wrapper.emitted("pagination")).toBeTruthy();
});
});Mock 技巧
Mock 模块
typescript
vi.mock("@/api/system/dict", () => ({
default: { getDictItems: vi.fn() },
}));
vi.mocked(DictAPI.getDictItems).mockResolvedValue([]);Mock 函数
typescript
const mockFn = vi.fn();
mockFn.mockReturnValue("result");
mockFn.mockResolvedValue("async result");
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(arg1, arg2);Mock 时间
typescript
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
vi.useRealTimers();断言速查
typescript
// 相等性
expect(value).toBe(expected);
expect(value).toEqual(expected);
// 真值
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeDefined();
// 数字
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(5);
// 字符串
expect(string).toMatch(/pattern/);
expect(string).toContain("substring");
// 数组
expect(array).toContain(item);
expect(array).toHaveLength(3);
// 对象
expect(obj).toHaveProperty("key");
expect(obj).toMatchObject({ key: value });
// 异步
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow();常见问题
如何 Mock localStorage?
typescript
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
});如何测试异步函数?
typescript
it("应该异步获取数据", async () => {
const result = await fetchData();
expect(result).toBeDefined();
});如何跳过测试?
typescript
it.skip("暂时跳过", () => {});