Files
spark-store/TESTING.md
momen cef68a95d9 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
2026-03-10 00:42:56 +08:00

8.3 KiB
Raw Permalink Blame History

测试文档

📋 目录

测试框架

Vitest单元测试

Vitest 是 Vite 原生的测试框架,提供快速的开发体验。

特点:

  • 与 Vite 配置共享
  • 极快的测试执行速度
  • 内置 TypeScript 支持
  • Jest 兼容的 API

配置文件: vitest.config.ts

PlaywrightE2E 测试)

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 测试最佳实践

  1. 使用选择器

    // 推荐:语义化选择器
    await page.click('[data-testid="install-button"]');
    
    // 避免:脆弱的选择器
    await page.click("button.btn-primary");
    
  2. 等待元素

    // 自动等待
    await expect(page.locator(".modal")).toBeVisible();
    
    // 手动等待(必要时)
    await page.waitForSelector(".modal", { state: "visible" });
    
  3. 截图和视频

    • 失败时自动截图
    • 失败时自动录制视频
  4. 网络拦截

    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 应用商店项目