mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
## 文档(全部中文) - AGENTS.md - 完整的 AI 编码指南(中文版) - CONTRIBUTING.md - 贡献指南 - DEVELOPMENT.md - 开发文档 - DEPLOYMENT.md - 部署文档 - TESTING.md - 测试文档 - TROUBLESHOOTING.md - 问题排查指南 - FAQ.md - 常见问题 - WORKFLOW.md - 标准开发流程文档 ## AI 工作流(9个详细工作流) - feature-development.md - 新功能开发流程 - bug-fix.md - Bug 修复流程 - code-review.md - 代码审查流程 - testing.md - 测试编写流程 - release.md - 发布流程 - refactoring.md - 代码重构流程 - documentation.md - 文档更新流程 - performance-optimization.md - 性能优化流程 - security-audit.md - 安全审计流程 ## 测试基础设施 - vitest.config.ts - Vitest 单元测试配置 - playwright.config.ts - Playwright E2E 测试配置 - src/__tests__/setup.ts - 测试环境设置 - src/__tests__/unit/downloadStatus.test.ts - 示例单元测试 - e2e/basic.spec.ts - 示例 E2E 测试 ## CI/CD - .github/workflows/test.yml - 新建测试 CI 工作流 - .github/workflows/build.yml - 更新构建工作流,添加测试步骤 ## Issue 模板 - 更新 bug_report.md 为标准 Bug 报告模板 - 更新 help_wanted.md 为标准功能请求模板 ## 配置更新 - package.json - 添加测试依赖和 7 个新的 npm 脚本 - .gitignore - 添加测试相关忽略项 ## 新增 npm 脚本 - test - 运行单元测试 - test:watch - 监听模式 - test:coverage - 生成覆盖率报告 - test:e2e - 运行 E2E 测试 - test:e2e:ui - E2E UI 模式 - test:e2e:debug - E2E 调试模式 - test:all - 运行所有测试 ## 新增测试依赖 - @playwright/test ^1.40.0 - @testing-library/jest-dom ^6.1.5 - @testing-library/vue ^8.0.1 - @vitest/coverage-v8 ^1.0.0 - @vue/test-utils ^2.4.3 - jsdom ^23.0.1 - vitest ^1.0.0
8.3 KiB
8.3 KiB
测试文档
📋 目录
测试框架
Vitest(单元测试)
Vitest 是 Vite 原生的测试框架,提供快速的开发体验。
特点:
- 与 Vite 配置共享
- 极快的测试执行速度
- 内置 TypeScript 支持
- Jest 兼容的 API
配置文件: vitest.config.ts
Playwright(E2E 测试)
Playwright 用于端到端测试,模拟真实用户操作。
特点:
- 支持多浏览器(Chromium, Firefox, WebKit)
- 自动等待
- 网络拦截和 mock
- 可视化测试运行
配置文件: playwright.config.ts
测试规范
命名规范
测试文件: *.test.ts 或 *.spec.ts
测试目录结构:
src/
├── __tests__/
│ ├── unit/ # 单元测试
│ │ ├── downloadStatus.test.ts
│ │ └── storeConfig.test.ts
│ ├── integration/ # 集成测试
│ │ └── installFlow.test.ts
│ └── setup.ts # 测试设置
└── components/
└── AppCard.test.ts # 组件测试
e2e/
├── install.spec.ts # E2E 测试
└── download.spec.ts
测试分组
使用 describe 分组相关测试:
describe("ComponentName", () => {
describe("method", () => {
it("should do something", () => {
// ...
});
});
});
测试命名
使用清晰的描述性名称:
✅ 好的:
it('should return true when app is installed')
it('should throw error when package not found')
❌ 不好的:
it('test1')
it('works')
编写测试
单元测试
测试纯函数:
import { describe, it, expect } from "vitest";
import { parseInstalledList } from "@/modules/parse";
describe("parseInstalledList", () => {
it("should parse installed list correctly", () => {
const output = "code/stable,1.108.2 amd64 [installed]";
const result = parseInstalledList(output);
expect(result).toHaveLength(1);
expect(result[0].pkgname).toBe("code");
expect(result[0].version).toBe("1.108.2");
});
});
测试 Vue 组件:
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import AppCard from "@/components/AppCard.vue";
import type { App } from "@/global/typedefinition";
describe("AppCard", () => {
const mockApp: App = {
name: "Test App",
pkgname: "test-app",
version: "1.0.0",
filename: "test.deb",
torrent_address: "",
author: "Test",
contributor: "Test",
website: "https://example.com",
update: "2024-01-01",
size: "100M",
more: "Test app",
tags: "",
img_urls: [],
icons: "",
category: "test",
currentStatus: "not-installed",
};
it("should render app name", () => {
const wrapper = mount(AppCard, {
props: {
app: mockApp,
},
});
expect(wrapper.text()).toContain("Test App");
});
it("should emit install event", async () => {
const wrapper = mount(AppCard, {
props: {
app: mockApp,
},
});
await wrapper.find(".install-button").trigger("click");
expect(wrapper.emitted("install")).toBeTruthy();
});
});
集成测试
测试模块间的交互:
import { describe, it, expect, vi, beforeEach } from "vitest";
import { installPackage } from "@/modules/processInstall";
import { downloads, addDownload } from "@/global/downloadStatus";
describe("installPackage integration", () => {
beforeEach(() => {
downloads.value = [];
vi.clearAllMocks();
});
it("should add download and send IPC message", () => {
const pkgname = "test-app";
installPackage(pkgname);
expect(downloads.value).toHaveLength(1);
expect(downloads.value[0].pkgname).toBe(pkgname);
expect(window.ipcRenderer.send).toHaveBeenCalledWith(
"queue-install",
expect.any(String),
);
});
});
运行测试
单元测试
# 运行所有测试
npm run test
# 监听模式(开发时)
npm run test:watch
# 运行特定文件
npm run test src/__tests__/unit/downloadStatus.test.ts
# 运行匹配模式的测试
npm run test -- downloadStatus
覆盖率
# 生成覆盖率报告
npm run test:coverage
# 报告位置:
# - 控制台: 文本报告
# - coverage/ 目录: HTML 报告
E2E 测试
# 运行所有 E2E 测试
npm run test:e2e
# UI 模式(推荐用于开发)
npm run test:e2e:ui
# 调试模式
npm run test:e2e:debug
# 运行特定测试
npm run test:e2e -- install.spec.ts
测试覆盖率
覆盖率目标
- 语句覆盖率: ≥ 70%
- 分支覆盖率: ≥ 70%
- 函数覆盖率: ≥ 70%
- 行覆盖率: ≥ 70%
查看报告
npm run test:coverage
# 在浏览器中打开
open coverage/index.html
CI 中强制检查
在 .github/workflows/test.yml 中配置覆盖率阈值。
Mock 数据
Mock IPC
在 src/__tests__/setup.ts 中全局 mock:
global.window = Object.create(window);
Object.defineProperty(window, "ipcRenderer", {
value: {
send: vi.fn(),
on: vi.fn(),
off: vi.fn(),
invoke: vi.fn(),
removeListener: vi.fn(),
},
});
Mock API 响应
import { vi } from "vitest";
import axios from "axios";
vi.mock("axios");
describe("fetchApps", () => {
it("should fetch apps from API", async () => {
const mockApps = [{ name: "Test", pkgname: "test" }];
axios.get.mockResolvedValue({ data: mockApps });
const result = await fetchApps();
expect(result).toEqual(mockApps);
});
});
Mock 文件系统
import { vi } from "vitest";
import fs from "node:fs";
vi.mock("node:fs");
describe("readConfig", () => {
it("should read config file", () => {
const mockConfig = { theme: "dark" };
fs.readFileSync.mockReturnValue(JSON.stringify(mockConfig));
const config = readConfig();
expect(config).toEqual(mockConfig);
});
});
E2E 测试
编写 E2E 测试
import { test, expect } from "@playwright/test";
test.describe("App Installation", () => {
test.beforeEach(async ({ page }) => {
await page.goto("http://127.0.0.1:3344");
});
test("should install an app", async ({ page }) => {
// 搜索应用
await page.fill('input[placeholder="搜索应用"]', "Test App");
await page.press('input[placeholder="搜索应用"]', "Enter");
// 等待结果
await expect(page.locator(".app-card")).toBeVisible();
// 点击安装
await page.click('.app-card:has-text("Test App") .install-button');
// 验证下载队列
await expect(page.locator(".download-queue")).toBeVisible();
await expect(page.locator(".download-item")).toHaveText("Test App");
});
test("should show installation progress", async ({ page }) => {
// ... 测试进度显示
});
test("should handle installation failure", async ({ page }) => {
// ... 测试失败处理
});
});
E2E 测试最佳实践
-
使用选择器
// 推荐:语义化选择器 await page.click('[data-testid="install-button"]'); // 避免:脆弱的选择器 await page.click("button.btn-primary"); -
等待元素
// 自动等待 await expect(page.locator(".modal")).toBeVisible(); // 手动等待(必要时) await page.waitForSelector(".modal", { state: "visible" }); -
截图和视频
- 失败时自动截图
- 失败时自动录制视频
-
网络拦截
await page.route("**/api/**", (route) => { route.fulfill({ status: 200, body: JSON.stringify(mockData), }); });
常见问题
测试超时
test(
"slow test",
async () => {
// 增加超时时间
},
{ timeout: 10000 },
);
异步测试
it("should handle async operation", async () => {
await someAsyncOperation();
expect(result).toBe(expected);
});
清理副作用
import { afterEach } from "vitest";
afterEach(() => {
// 清理 mock
vi.restoreAllMocks();
// 清理状态
downloads.value = [];
});
© 2026 APM 应用商店项目