testing-automation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Automation
测试自动化
Patterns and best practices for testing TypeScript projects with Bun test runner, including coverage thresholds, mocking strategies, and CI/CD integration.
使用Bun测试运行器测试TypeScript项目的模式与最佳实践,包括覆盖率阈值、模拟策略以及CI/CD集成。
When to use this skill
何时使用此技能
Use this skill when:
- Writing unit tests or integration tests
- Organizing test file structure
- Configuring test coverage thresholds
- Mocking dependencies for CLI testing
- Setting up CI/CD test pipelines
- Debugging failing tests
在以下场景使用此技能:
- 编写单元测试或集成测试
- 组织测试文件结构
- 配置测试覆盖率阈值
- 为CLI测试模拟依赖项
- 设置CI/CD测试流水线
- 调试失败的测试
Bun Test Runner Basics
Bun测试运行器基础
Running Tests
运行测试
bash
undefinedbash
undefinedRun all tests
Run all tests
bun test
bun test
Run tests in watch mode
Run tests in watch mode
bun test --watch
bun test --watch
Run tests with coverage
Run tests with coverage
bun test --coverage
bun test --coverage
Run specific test file
Run specific test file
bun test path/to/specific.test.ts
bun test path/to/specific.test.ts
Run tests matching pattern
Run tests matching pattern
bun test --pattern "**/*command.test.ts"
undefinedbun test --pattern "**/*command.test.ts"
undefinedTest File Organization
测试文件组织
typescript
// src/cli.test.ts (test file alongside source)
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
describe('CLI Command', () => {
beforeEach(() => {
// Setup before each test
});
afterEach(() => {
// Cleanup after each test
});
it('should parse command arguments correctly', async () => {
const result = parseCommand(['create', 'test']);
expect(result.name).toBe('test');
});
});typescript
// src/cli.test.ts (test file alongside source)
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
describe('CLI Command', () => {
beforeEach(() => {
// Setup before each test
});
afterEach(() => {
// Cleanup after each test
});
it('should parse command arguments correctly', async () => {
const result = parseCommand(['create', 'test']);
expect(result.name).toBe('test');
});
});Test Types
测试类型
typescript
// Unit tests - test individual functions
it('should validate skill name format', () => {
expect(validateSkillName('test-skill')).toBe(true);
expect(validateSkillName('Invalid_Name')).toBe(false);
});
// Integration tests - test workflows
it('should create skill with all subdirectories', async () => {
await createSkill('test-skill');
const skillExists = await fs.exists('.skills/test-skill/SKILL.md');
expect(skillExists).toBe(true);
});
// Smoke tests - basic functionality tests
it('should run without errors', async () => {
const result = await executeCommand(['list']);
expect(result.exitCode).toBe(0);
});typescript
// Unit tests - test individual functions
it('should validate skill name format', () => {
expect(validateSkillName('test-skill')).toBe(true);
expect(validateSkillName('Invalid_Name')).toBe(false);
});
// Integration tests - test workflows
it('should create skill with all subdirectories', async () => {
await createSkill('test-skill');
const skillExists = await fs.exists('.skills/test-skill/SKILL.md');
expect(skillExists).toBe(true);
});
// Smoke tests - basic functionality tests
it('should run without errors', async () => {
const result = await executeCommand(['list']);
expect(result.exitCode).toBe(0);
});Test Coverage
测试覆盖率
Coverage Thresholds
覆盖率阈值
From qa-subagent configuration in :
agents/qa-subagent.jsontypescript
// Target: 80% code coverage
const coverageThreshold = 80;来自中的qa-subagent配置:
agents/qa-subagent.jsontypescript
// Target: 80% code coverage
const coverageThreshold = 80;Generating Coverage Reports
生成覆盖率报告
bash
undefinedbash
undefinedRun tests with coverage
Run tests with coverage
bun test --coverage
bun test --coverage
Coverage output in coverage/ directory
Coverage output in coverage/ directory
- coverage/index.html - HTML report
- coverage/index.html - HTML report
- coverage/coverage-final.json - JSON for CI
- coverage/coverage-final.json - JSON for CI
undefinedundefinedEnforcing Coverage in CI
在CI中强制实施覆盖率
yaml
undefinedyaml
undefined.github/workflows/test.yml
.github/workflows/test.yml
- name: Check coverage run: bun test --coverage
- name: Verify threshold run: | COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct') if (( $(echo "$COVERAGE < 80" | bc -l) )); then echo "Coverage $(COVERAGE)% is below 80%" exit 1 fi
undefined- name: Check coverage run: bun test --coverage
- name: Verify threshold run: | COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct') if (( $(echo "$COVERAGE < 80" | bc -l) )); then echo "Coverage $(COVERAGE)% is below 80%" exit 1 fi
undefinedMocking and Stubbing
模拟与存根
Mocking File System
模拟文件系统
typescript
import { mockFS } from 'bun:test/mock';
mockFS({
'.skills/test-skill/SKILL.md': 'content here',
}, async () => {
await createSkill('test-skill');
const content = await fs.readFile('.skills/test-skill/SKILL.md', 'utf-8');
expect(content).toBe('content here');
});typescript
import { mockFS } from 'bun:test/mock';
mockFS({
'.skills/test-skill/SKILL.md': 'content here',
}, async () => {
await createSkill('test-skill');
const content = await fs.readFile('.skills/test-skill/SKILL.md', 'utf-8');
expect(content).toBe('content here');
});Mocking CLI Input
模拟CLI输入
typescript
import { mockProcess } from 'bun:test/mock';
mockProcess({
argv: ['create', 'test-skill'],
stdin: 'y\n', // Simulate user input
});
await executeCommand();
// Verify command behavior with mocked inputtypescript
import { mockProcess } from 'bun:test/mock';
mockProcess({
argv: ['create', 'test-skill'],
stdin: 'y\n', // Simulate user input
});
await executeCommand();
// Verify command behavior with mocked inputMocking External Services
模拟外部服务
typescript
import { mock, spyOn } from 'bun:test';
// Mock Context7 API
const mockContext7 = mock(() => ({
resolveLibrary: async (name: string) => ({ id: name, docs: '...' }),
}));
// Use mock in test
const result = await mockContext7.resolveLibrary('react');
expect(result.docs).toBeDefined();typescript
import { mock, spyOn } from 'bun:test';
// Mock Context7 API
const mockContext7 = mock(() => ({
resolveLibrary: async (name: string) => ({ id: name, docs: '...' }),
}));
// Use mock in test
const result = await mockContext7.resolveLibrary('react');
expect(result.docs).toBeDefined();CLI Testing Patterns
CLI测试模式
Capturing Output
捕获输出
typescript
import { stdout, stderr } from 'bun:test';
const spyStdout = spyOn(stdout, 'write');
const spyStderr = spyOn(stderr, 'write');
await executeCommand(['list']);
expect(spyStdout).toHaveBeenCalled();
expect(spyStderr).not.toHaveBeenCalled();typescript
import { stdout, stderr } from 'bun:test';
const spyStdout = spyOn(stdout, 'write');
const spyStderr = spyOn(stderr, 'write');
await executeCommand(['list']);
expect(spyStdout).toHaveBeenCalled();
expect(spyStderr).not.toHaveBeenCalled();Testing Exit Codes
测试退出码
typescript
import { mockProcess } from 'bun:test/mock';
mockProcess({
exit: (code: number) => {
exitCode = code;
},
});
await executeCommand(['invalid-command']);
expect(exitCode).toBe(1);typescript
import { mockProcess } from 'bun:test/mock';
mockProcess({
exit: (code: number) => {
exitCode = code;
},
});
await executeCommand(['invalid-command']);
expect(exitCode).toBe(1);Testing Error Messages
测试错误消息
typescript
it('should show error message on invalid input', async () => {
const spyConsoleError = spyOn(console, 'error');
await executeCommand(['create', 'Invalid_Name']);
expect(spyConsoleError).toHaveBeenCalledWith(
expect.stringContaining('Invalid skill name')
);
});typescript
it('should show error message on invalid input', async () => {
const spyConsoleError = spyOn(console, 'error');
await executeCommand(['create', 'Invalid_Name']);
expect(spyConsoleError).toHaveBeenCalledWith(
expect.stringContaining('Invalid skill name')
);
});CI/CD Integration
CI/CD集成
GitHub Actions Test Workflow
GitHub Actions测试工作流
yaml
undefinedyaml
undefined.github/workflows/test.yml
.github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
undefinedname: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
undefinedTest Matrix Strategy
测试矩阵策略
yaml
undefinedyaml
undefinedTest across multiple Node versions and OS
Test across multiple Node versions and OS
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
bun-version: ['1.0.x', 'latest']
steps:
- name: Run tests run: bun test
undefinedstrategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
bun-version: ['1.0.x', 'latest']
steps:
- name: Run tests run: bun test
undefinedQuality Gates
质量门禁
Pre-commit Test Hooks
提交前测试钩子
json
// package.json
{
"scripts": {
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage",
"precommit": "bun test"
}
}json
// package.json
{
"scripts": {
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage",
"precommit": "bun test"
}
}Linting Tests
测试代码检查
bash
undefinedbash
undefinedRun linter on test files
Run linter on test files
bun lint src/**/*.test.ts
bun lint src/**/*.test.ts
Format test files
Format test files
bun format src/**/*.test.ts
undefinedbun format src/**/*.test.ts
undefinedVerification
验证
After implementing test infrastructure:
- Test files organized alongside source code
- Coverage threshold (80%) is enforced
- Mocks are used for external dependencies
- CI/CD runs tests on every PR
- Output capture tests verify correct formatting
- Exit code tests validate error handling
- Pre-commit hooks prevent broken tests from committing
在实现测试基础设施后:
- 测试文件与源代码放在同一目录下
- 覆盖率阈值(80%)已被强制实施
- 外部依赖项使用了模拟
- CI/CD在每个PR上运行测试
- 输出捕获测试验证了格式正确性
- 退出码测试验证了错误处理
- 提交前钩子阻止了有问题的测试被提交
Examples from liaison-toolkit
liaison-toolkit示例
Example 1: Testing Skill List Command
示例1:测试技能列表命令
typescript
// packages/liaison/__tests__/skill.test.ts
import { describe, it, expect, mock } from 'bun:test';
describe('skill list command', () => {
it('should list all available skills', async () => {
// Mock discoverSkills
mock(() => ({
'.skills/library-research/SKILL.md': '...',
'.skills/git-automation/SKILL.md': '...',
}), async () => {
const result = await executeCommand(['skill', 'list']);
expect(result.skills.length).toBeGreaterThan(0);
});
});
});typescript
// packages/liaison/__tests__/skill.test.ts
import { describe, it, expect, mock } from 'bun:test';
describe('skill list command', () => {
it('should list all available skills', async () => {
// Mock discoverSkills
mock(() => ({
'.skills/library-research/SKILL.md': '...',
'.skills/git-automation/SKILL.md': '...',
}), async () => {
const result = await executeCommand(['skill', 'list']);
expect(result.skills.length).toBeGreaterThan(0);
});
});
});Example 2: Testing Validation
示例2:测试验证逻辑
typescript
import { describe, it, expect } from 'bun:test';
describe('skill validation', () => {
it('should reject invalid skill names', async () => {
const result = await validateSkillName('Invalid_Name');
expect(result.valid).toBe(false);
expect(result.errors[0].type).toBe('invalid-name');
});
it('should accept valid skill names', async () => {
const result = await validateSkillName('test-skill');
expect(result.valid).toBe(true);
});
});typescript
import { describe, it, expect } from 'bun:test';
describe('skill validation', () => {
it('should reject invalid skill names', async () => {
const result = await validateSkillName('Invalid_Name');
expect(result.valid).toBe(false);
expect(result.errors[0].type).toBe('invalid-name');
});
it('should accept valid skill names', async () => {
const result = await validateSkillName('test-skill');
expect(result.valid).toBe(true);
});
});