| name | tzurot-testing |
| description | Use when writing tests, debugging test failures, mocking dependencies, or using fake timers. Covers Vitest patterns, mock factories, and promise rejection handling. |
| lastUpdated | 2026-01-02 |
Tzurot v3 Testing Patterns
Use this skill when: Writing tests, debugging test failures, adding mocks, or working with fake timers.
Quick Reference
pnpm test
pnpm --filter @tzurot/ai-worker test
pnpm test -- MyService.test.ts
pnpm test:coverage
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.ts next to MyService.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)
const promise = asyncFunction();
await vi.runAllTimersAsync();
await expect(promise).rejects.toThrow();
const promise = asyncFunction();
const assertion = expect(promise).rejects.toThrow('Error');
await vi.runAllTimersAsync();
await assertion;
Mock Factory Pattern
vi.mock('./MyService.js', async () => {
const { mockMyService } = await import('../test/mocks/MyService.mock.js');
return mockMyService;
});
import { getMyServiceMock } from '../test/mocks/index.js';
it('should call service', () => {
expect(getMyServiceMock().someMethod).toHaveBeenCalled();
});
Common Mocks
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;
}
function createMockPrisma() {
return {
personality: { findUnique: vi.fn(), findMany: vi.fn() },
$disconnect: vi.fn(),
} as unknown as PrismaClient;
}
function createMockRedis() {
return {
get: vi.fn().mockResolvedValue(null),
set: vi.fn().(),
: vi.().(),
} ;
}
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) |
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/.
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 };
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/.
describe('AI generation flow', () => {
it('should process job through full pipeline', async () => {
const jobData = createTestGenerationJob();
const result = await processGenerationJob(jobData);
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
Integration tests use PGLite (in-memory PostgreSQL with pgvector) for zero-setup database testing:
pnpm test:integration
./scripts/testing/regenerate-pglite-schema.sh
Schema Management (CRITICAL):
- Schema SQL is auto-generated from
prisma/schema.prisma
- Stored in
tests/integration/schema/pglite-schema.sql
- Regenerate after Prisma migrations:
./scripts/testing/regenerate-pglite-schema.sh
- Uses
prisma migrate diff --from-empty --to-schema - never write SQL manually
Environment Detection:
| Environment | Database | Redis |
|---|
| Local (no DATABASE_URL) | PGLite | Mock |
| Local (with DATABASE_URL) | Real Postgres | Mock |
| CI (GITHUB_ACTIONS=true) | Service Postgres | Real |
Setup in Tests:
import { setupTestEnvironment, type TestEnvironment } from './setup';
let testEnv: TestEnvironment;
beforeAll(async () => {
testEnv = await setupTestEnvironment();
});
afterAll(async () => {
await testEnv.cleanup();
});
Contract Coverage Audit
The project uses a ratchet system to prevent new APIs from being added without contract tests:
npx tsx scripts/testing/audit-contract-coverage.ts
npx tsx scripts/testing/audit-contract-coverage.ts --update-baseline
npx tsx scripts/testing/audit-contract-coverage.ts --strict
How It Works:
- Finds all Zod schemas in
packages/common-types/src/schemas/api/
- Checks which have
.safeParse() calls in contract tests
- Compares against
contract-coverage-baseline.json
- Fails CI if NEW untested schemas are added
Adding Contract Tests:
import { MyResponseSchema } from '../schemas/api/myFeature.js';
describe('MyFeature API Contract', () => {
it('should validate response structure', () => {
const response = { id: '123', name: 'Test' };
expect(MyResponseSchema.safeParse(response).success).toBe(true);
});
it('should reject invalid response', () => {
const invalid = { id: 123 };
expect(MyResponseSchema.safeParse(invalid).success).toBe(false);
});
});
After Adding Tests: Update baseline if needed to remove fixed gaps
Anti-Patterns
expect(service['privateMethod']()).toBe(value);
await new Promise(r => setTimeout(r, 1000));
console.log('Debug:', value);
it.skip('broken test', () => {});
Coverage Requirements (CI Enforced)
pnpm test:coverage
pnpm --filter @tzurot/api-gateway test:coverage
| Target | Threshold | Enforcement |
|---|
| Project | 80% | Codecov blocks if drops >2% |
| Patch | 80% | New code must be 80%+ covered |
| Services | 80% | Tracked per-service (ai-worker, api-gateway, bot-client) |
| Utils | 90% | Higher bar for shared utilities |
CI Gate: Codecov runs on every PR. Coverage report shows:
- Overall project coverage change
- Per-file coverage for changed files
- Patch coverage (new/modified lines only)
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
- Contract audit:
scripts/testing/audit-contract-coverage.ts
- Schema regeneration:
scripts/testing/regenerate-pglite-schema.sh