testing-automation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing 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
undefined
bash
undefined

Run 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"
undefined
bun test --pattern "**/*command.test.ts"
undefined

Test 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.json
:
typescript
// Target: 80% code coverage
const coverageThreshold = 80;
来自
agents/qa-subagent.json
中的qa-subagent配置:
typescript
// Target: 80% code coverage
const coverageThreshold = 80;

Generating Coverage Reports

生成覆盖率报告

bash
undefined
bash
undefined

Run 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

undefined
undefined

Enforcing Coverage in CI

在CI中强制实施覆盖率

yaml
undefined
yaml
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
undefined

Mocking 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 input
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 input

Mocking 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
undefined
yaml
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
undefined
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
undefined

Test Matrix Strategy

测试矩阵策略

yaml
undefined
yaml
undefined

Test 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
undefined
strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] bun-version: ['1.0.x', 'latest']
steps:
  • name: Run tests run: bun test
undefined

Quality 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
undefined
bash
undefined

Run 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
undefined
bun format src/**/*.test.ts
undefined

Verification

验证

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);
  });
});

Related Resources

相关资源