Skip to content

单元测试

本文档介绍 vue3-element-admin 的单元测试配置、使用方法和最佳实践。

测试框架

依赖说明
Vitest基于 Vite 的快速单元测试框架
@vue/test-utilsVue 组件测试工具
happy-dom轻量级 DOM 环境(比 jsdom 快)
@testing-library/vueVue 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             # 测试环境配置

测试覆盖

已完成

模块文件覆盖内容
utilsstorage.test.tslocalStorage/sessionStorage 操作、批量清理
utilsvalidate.test.tsURL、邮箱、手机号验证
utilsformat.test.ts增长率、文件大小、数字格式化
utilsauth.test.tsToken 管理、权限判断
storedict.test.ts字典缓存、防重复请求
storeapp.test.ts侧边栏、设备、语言管理
storesettings.test.ts主题、布局、特殊模式
composablesuseTableSelection.test.ts表格选择管理
composablesuseDictSync.test.ts字典同步
componentsPagination.test.ts分页组件

待添加

  • user.test.ts - 用户 Store
  • permission.test.ts - 权限 Store
  • DictSelect.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("暂时跳过", () => {});

参考资料

基于 MIT 许可发布