tzurot-testing
Contains MANDATORY Vitest testing patterns for Tzurot v3. MUST be consulted before creating any `.test.ts` file. Covers mock factories, fake timers, and promise rejection handling.
安装与使用
请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。
查看来源 ↗使用指南技能说明
Tzurot v3 Testing Patterns
Use this skill when: Writing tests, debugging test failures, adding mocks, or working with fake timers.
Quick Reference
# Run all tests
pnpm test
# Run specific service
pnpm --filter @tzurot/ai-worker test
# Run specific file
pnpm test -- MyService.test.ts
# Coverage
pnpm test:coverage
// Basic test structure
import { describe, it, expect, vi, beforeEach } from 'vitest';
describe('MyService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should do something', () => {
expect(result).toBe(expected);
});
});
Core Principles
- Test behavior, not implementation
-
Colocated tests -
MyService.test.tsnext toMyService.ts - Mock all external dependencies - Discord, Redis, Prisma, AI
- Use fake timers - No real delays in tests
Essential Patterns
Fake Timers (ALWAYS Use)
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should retry with delay', async () => {
const promise = withRetry(fn);
await vi.runAllTimersAsync();
await promise;
});
Promise Rejections with Fake Timers (CRITICAL)
// ❌ WRONG - Causes PromiseRejectionHandledWarning
const promise = asyncFunction();
await vi.runAllTimersAsync(); // Rejection happens here!
await expect(promise).rejects.toThrow(); // Too late
// ✅ CORRECT - Attach handler BEFORE advancing timers
const promise = asyncFunction();
const assertion = expect(promise).rejects.toThrow('Error'); // Handler attached
await vi.runAllTimersAsync(); // Now advance
await assertion; // Await result
Mock Factory Pattern
// Use async factory for vi.mock hoisting
vi.mock('./MyService.js', async () => {
const { mockMyService } = await import('../test/mocks/MyService.mock.js');
return mockMyService;
});
// Import accessors after vi.mock
import { getMyServiceMock } from '../test/mocks/index.js';
it('should call service', () => {
expect(getMyServiceMock().someMethod).toHaveBeenCalled();
});
Common Mocks
// Discord message
function createMockMessage(overrides = {}) {
return {
id: '123',
content: 'test',
author: { id: 'user-123', bot: false },
channel: { id: 'channel-123', send: vi.fn() },
reply: vi.fn().mockResolvedValue({}),
...overrides,
} as unknown as Message;
}
// Prisma
function createMockPrisma() {
return {
personality: { findUnique: vi.fn(), findMany: vi.fn() },
$disconnect: vi.fn(),
} as unknown as PrismaClient;
}
// Redis
function createMockRedis() {
return {
get: vi.fn().mockResolvedValue(null),
set: vi.fn().mockResolvedValue('OK'),
ping: vi.fn().mockResolvedValue('PONG'),
} as unknown as Redis;
}
Test File Naming
| Type | Pattern | Location |
|---|---|---|
| Unit | *.test.ts |
Next to source |
| Component | *.component.test.ts |
Next to source |
| Integration | *.test.ts |
tests/integration/ |
| Contract | *.contract.test.ts |
common-types/types/ |
Mock Reset Functions
| Function | What It Does | When to Use |
|---|---|---|
vi.clearAllMocks() |
Clears call history, keeps impl | beforeEach() |
vi.restoreAllMocks() |
Restores original (spies only) | afterEach() |
vi.resetAllMocks() |
Clears history + resets impl | Rarely needed |
When to Add Tests
| Change | Unit | Contract | Integration |
|---|---|---|---|
| New API endpoint | ✅ | ✅ Required | ✅ If DB/multi-service |
New *.service.ts |
✅ | If shared | ✅ For complex DB operations |
| New utility/helper | ✅ | No | No |
| Bug fix | ✅ | If contract | If multi-component interaction |
| New dashboard/modal | ✅ | If API types | No (UI logic, mock sessions) |
| New tooling | ✅ | No | No |
Tooling Package Tests
All tooling code requires unit tests. See tzurot-tooling skill for details. Key points:
- Implementation modules in
packages/tooling/src/*/need*.test.tsfiles - Command registration files (
commands/*.ts) are thin wrappers - no tests needed - Mock
child_processfunctions likeexecSyncandspawnSyncfor shell commands
Integration Test Guidance
When Required:
- Database operations with complex queries (joins, transactions)
- Cross-service communication (bot-client → api-gateway → ai-worker)
- Business logic spanning multiple services
When NOT Needed:
- Pure utility functions
- UI/Discord interaction handlers (mock the session/API instead)
- Simple CRUD operations
Future Enhancement: Service-pairing ratchet where every *.service.ts requires *.integration.test.ts
Contract Tests
Contract tests verify API boundaries between services. Located in common-types/types/.
// *.contract.test.ts - Verify schema compatibility
import { PersonaResponseSchema } from './schemas.js';
describe('PersonaResponse contract', () => {
it('should parse valid API response', () => {
const response = { id: 'uuid', name: 'Test', preferredName: null };
expect(() => PersonaResponseSchema.parse(response)).not.toThrow();
});
it('should reject invalid response', () => {
const response = { id: 123 }; // Wrong type
expect(() => PersonaResponseSchema.parse(response)).toThrow();
});
});
When to write: New API endpoints, schema changes, cross-service communication.
Purpose: Catch breaking changes before they hit production. If bot-client expects { name: string } but api-gateway returns { displayName: string }, contract tests fail.
Integration Tests
Integration tests verify multiple components working together. Located in tests/integration/.
// Test actual service interactions (with mocked externals)
describe('AI generation flow', () => {
it('should process job through full pipeline', async () => {
// Setup: Create test job data
const jobData = createTestGenerationJob();
// Act: Process through actual handlers (mocking only AI/Discord)
const result = await processGenerationJob(jobData);
// Assert: Verify end-to-end behavior
expect(result.response).toBeDefined();
expect(mockDiscordWebhook).toHaveBeenCalled();
});
});
When to write: Complex workflows, cross-service operations, database interactions.
Key difference:
- Unit tests: Mock all dependencies, test one function
- Integration tests: Use real components (except external APIs like Discord, OpenRouter)
PGLite for Local Integration Tests
# Run integration tests (no DATABASE_URL needed)
pnpm test:integration
# Regenerate schema after Prisma migrations
./scripts/testing/regenerate-pglite-schema.sh
📚 See: docs/reference/testing/PGLITE_SETUP.md for full setup, environment detection, and test patterns.
Definition of Done
Before marking a feature complete:
- New service files have
.component.test.ts - New API schemas have
.contract.test.ts(if crossing service boundary) - Complex DB operations have integration test coverage
- Coverage doesn't drop (Codecov enforces 80% threshold)
- Run
pnpm ops test:auditto verify no new test gaps
Test Coverage Audits (Ratchet System)
The project uses ratchets to prevent new untested code:
# Run both audits (CI does this automatically)
pnpm ops test:audit
# Contract coverage only
pnpm ops test:audit-contracts
# Service integration coverage only
pnpm ops test:audit-services
# Update baseline (after closing gaps)
pnpm ops test:audit-contracts --update
pnpm ops test:audit-services --update
# Strict mode (fails on ANY gap, not just new ones)
pnpm ops test:audit --strict
📚 See: docs/reference/testing/COVERAGE_AUDIT_SYSTEM.md for detailed audit workflows, chip-away process, and priority order.
Anti-Patterns
// ❌ BAD - Testing private methods
expect(service['privateMethod']()).toBe(value);
// ❌ BAD - Real delays
await new Promise(r => setTimeout(r, 1000));
// ❌ BAD - console.log in tests
console.log('Debug:', value);
// ❌ BAD - Skipping instead of fixing
it.skip('broken test', () => {});
Coverage Requirements
Threshold: 80% project-wide and per-patch. Codecov blocks if coverage drops >2%.
pnpm test:coverage # Check coverage locally
pnpm --filter @tzurot/api-gateway test:coverage # Specific service
📚 See: docs/reference/testing/COVERAGE_AUDIT_SYSTEM.md for detailed thresholds and reading coverage data.
Related Skills
- tzurot-code-quality - Lint rules, refactoring patterns
- tzurot-types - Type-safe test fixtures
- tzurot-git-workflow - Run tests before pushing
- tzurot-observability - Mock logger in tests
References
- Full testing guide:
docs/guides/TESTING.md - Mock factories:
services/*/src/test/mocks/ - Global philosophy:
~/.claude/CLAUDE.md#universal-testing-philosophy - PGLite setup:
tests/integration/setup.ts - Test audit commands:
pnpm ops test:audit-* - Schema regeneration:
./scripts/testing/regenerate-pglite-schema.sh - Contract baseline:
contract-coverage-baseline.json - Service baseline:
service-integration-baseline.json
使用示例
查看来源仓库中的使用示例与配置说明。
查看来源 ↗来源历史
- 收录时间
- 2026-01-20
- 索引更新时间
- 2026-01-26
- Branch
- main
收录与同步日期属于本站记录。源码修改历史请查看来源仓库。
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!