mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
chore: add comprehensive documentation and testing infrastructure
## 文档(全部中文) - 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
This commit is contained in:
436
TESTING.md
Normal file
436
TESTING.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# 测试文档
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [测试框架](#测试框架)
|
||||
- [测试规范](#测试规范)
|
||||
- [编写测试](#编写测试)
|
||||
- [运行测试](#运行测试)
|
||||
- [测试覆盖率](#测试覆盖率)
|
||||
- [Mock 数据](#mock-数据)
|
||||
- [E2E 测试](#e2e-测试)
|
||||
|
||||
## 测试框架
|
||||
|
||||
### 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` 分组相关测试:
|
||||
|
||||
```typescript
|
||||
describe("ComponentName", () => {
|
||||
describe("method", () => {
|
||||
it("should do something", () => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 测试命名
|
||||
|
||||
使用清晰的描述性名称:
|
||||
|
||||
```typescript
|
||||
✅ 好的:
|
||||
it('should return true when app is installed')
|
||||
it('should throw error when package not found')
|
||||
|
||||
❌ 不好的:
|
||||
it('test1')
|
||||
it('works')
|
||||
```
|
||||
|
||||
## 编写测试
|
||||
|
||||
### 单元测试
|
||||
|
||||
**测试纯函数:**
|
||||
|
||||
```typescript
|
||||
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 组件:**
|
||||
|
||||
```typescript
|
||||
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();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
测试模块间的交互:
|
||||
|
||||
```typescript
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 单元测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
npm run test
|
||||
|
||||
# 监听模式(开发时)
|
||||
npm run test:watch
|
||||
|
||||
# 运行特定文件
|
||||
npm run test src/__tests__/unit/downloadStatus.test.ts
|
||||
|
||||
# 运行匹配模式的测试
|
||||
npm run test -- downloadStatus
|
||||
```
|
||||
|
||||
### 覆盖率
|
||||
|
||||
```bash
|
||||
# 生成覆盖率报告
|
||||
npm run test:coverage
|
||||
|
||||
# 报告位置:
|
||||
# - 控制台: 文本报告
|
||||
# - coverage/ 目录: HTML 报告
|
||||
```
|
||||
|
||||
### E2E 测试
|
||||
|
||||
```bash
|
||||
# 运行所有 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%
|
||||
|
||||
### 查看报告
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
|
||||
# 在浏览器中打开
|
||||
open coverage/index.html
|
||||
```
|
||||
|
||||
### CI 中强制检查
|
||||
|
||||
在 `.github/workflows/test.yml` 中配置覆盖率阈值。
|
||||
|
||||
## Mock 数据
|
||||
|
||||
### Mock IPC
|
||||
|
||||
在 `src/__tests__/setup.ts` 中全局 mock:
|
||||
|
||||
```typescript
|
||||
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 响应
|
||||
|
||||
```typescript
|
||||
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 文件系统
|
||||
|
||||
```typescript
|
||||
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 测试
|
||||
|
||||
```typescript
|
||||
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 测试最佳实践
|
||||
|
||||
1. **使用选择器**
|
||||
|
||||
```typescript
|
||||
// 推荐:语义化选择器
|
||||
await page.click('[data-testid="install-button"]');
|
||||
|
||||
// 避免:脆弱的选择器
|
||||
await page.click("button.btn-primary");
|
||||
```
|
||||
|
||||
2. **等待元素**
|
||||
|
||||
```typescript
|
||||
// 自动等待
|
||||
await expect(page.locator(".modal")).toBeVisible();
|
||||
|
||||
// 手动等待(必要时)
|
||||
await page.waitForSelector(".modal", { state: "visible" });
|
||||
```
|
||||
|
||||
3. **截图和视频**
|
||||
- 失败时自动截图
|
||||
- 失败时自动录制视频
|
||||
|
||||
4. **网络拦截**
|
||||
```typescript
|
||||
await page.route("**/api/**", (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(mockData),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 测试超时
|
||||
|
||||
```typescript
|
||||
test(
|
||||
"slow test",
|
||||
async () => {
|
||||
// 增加超时时间
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
```
|
||||
|
||||
### 异步测试
|
||||
|
||||
```typescript
|
||||
it("should handle async operation", async () => {
|
||||
await someAsyncOperation();
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
```
|
||||
|
||||
### 清理副作用
|
||||
|
||||
```typescript
|
||||
import { afterEach } from "vitest";
|
||||
|
||||
afterEach(() => {
|
||||
// 清理 mock
|
||||
vi.restoreAllMocks();
|
||||
// 清理状态
|
||||
downloads.value = [];
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**© 2026 APM 应用商店项目**
|
||||
Reference in New Issue
Block a user