📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-05-29 08:33:53 +00:00
parent fdb52f1e96
commit 06e0d13d57
1615 changed files with 232858 additions and 0 deletions
@@ -0,0 +1,357 @@
---
name: tdd
description: This skill should be used when implementing features with TDD, writing tests first, or refactoring with test coverage. Applies disciplined Red-Green-Refactor cycles with TypeScript/Bun and Rust tooling.
metadata:
version: "2.1.0"
---
# Test-Driven Development
Write tests first, implement minimal code to pass, refactor systematically.
<when_to_use>
- New features with TDD methodology
- Complex business logic requiring coverage
- Critical paths: auth, payments, data integrity
- Bug fixes: reproduce with test, fix, verify
- Refactoring: ensure behavior preservation
- API design: tests define the interface
NOT for: exploratory coding, UI prototypes, static config, trivial glue code
</when_to_use>
<stages>
Load the **maintain-tasks** skill for stage tracking. Advance through RED-GREEN-REFACTOR cycle.
| Stage | Trigger | activeForm |
|-------|---------|------------|
| Red | Session start / cycle restart | "Writing failing test" |
| Green | Test written and failing | "Implementing code" |
| Refactor | Tests passing | "Refactoring code" |
| Verify | Refactor complete | "Verifying implementation" |
Task format:
```text
- Write failing test for { feature }
- Implement { feature } to pass tests
- Refactor { aspect }
- Verify { what's being checked }
```
Workflow:
- Start: Create "Red" stage `in_progress`
- Transition: Mark current `completed`, add next `in_progress`
- After each stage: Run tests before advancing
- Multiple cycles: Return to "Red" for next feature
Edge cases:
- Good existing tests: Start at "Refactor" after confirming pass
- Bug fix: Start at "Red" with failing test reproducing bug
- No regression: Tests must continue passing through all stages
</stages>
<cycle>
```
RED --> GREEN --> REFACTOR --> RED --> ...
| | |
Test Impl Improve
Fails Passes Quality
```
Each cycle: 5-15 min. Longer = step too large, decompose.
Philosophy:
- Red-Green-Refactor as primary workflow
- Test quality over quantity - behavior, not implementation
- Incremental progress - small focused cycles
- Type safety throughout - tests as type-safe as production
</cycle>
<red_phase>
Write tests defining desired behavior before implementation exists.
Guidelines:
- 3-5 related tests fully specifying one feature
- Type system makes invalid states unrepresentable
- Each test = one specific behavior
- Run tests, verify fail for right reason
- Descriptive names forming sentences
TypeScript:
```typescript
import { describe, test, expect } from 'bun:test'
describe('UserAuthentication', () => {
test('authenticates with valid credentials', async () => {
const result = await authenticate({ email: 'user@example.com', password: 'SecurePass123!' })
expect(result).toMatchObject({ type: 'success', user: expect.objectContaining({ email: 'user@example.com' }) })
})
test('rejects invalid credentials', async () => {
const result = await authenticate({ email: 'wrong@example.com', password: 'wrong' })
expect(result).toMatchObject({ type: 'error', code: 'INVALID_CREDENTIALS' })
})
test.todo('implements rate limiting after failed attempts')
})
```
Rust:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn authenticates_with_valid_credentials() {
let creds = Credentials { email: "user@example.com".into(), password: "SecurePass123!".into() };
assert!(matches!(authenticate(&creds), Ok(AuthResult::Success { .. })));
}
#[test]
fn rejects_invalid_credentials() {
let creds = Credentials { email: "wrong@example.com".into(), password: "wrong".into() };
assert!(matches!(authenticate(&creds), Err(AuthError::InvalidCredentials)));
}
}
```
Commit: `test: add failing tests for [feature]`
Transition: Mark "Red" `completed`, create "Green" `in_progress`
</red_phase>
<green_phase>
Implement minimum code to make tests pass.
Guidelines:
- Focus on passing tests, not perfect code
- Explicit types where aids clarity
- Straightforward solutions first
- Hardcode if passes test - refactor generalizes
- Run tests frequently
TypeScript:
```typescript
type AuthResult = { type: 'success'; user: User } | { type: 'error'; code: string }
async function authenticate(creds: { email: string; password: string }): Promise<AuthResult> {
if (!creds.password) return { type: 'error', code: 'MISSING_PASSWORD' }
const user = await findUserByEmail(creds.email)
if (!user) return { type: 'error', code: 'INVALID_CREDENTIALS' }
const match = await comparePassword(creds.password, user.passwordHash)
if (!match) return { type: 'error', code: 'INVALID_CREDENTIALS' }
return { type: 'success', user }
}
```
Rust:
```rust
pub fn authenticate(creds: &Credentials) -> Result<AuthResult, AuthError> {
if creds.password.is_empty() { return Err(AuthError::MissingPassword); }
let user = find_user_by_email(&creds.email).ok_or(AuthError::InvalidCredentials)?;
if !compare_password(&creds.password, &user.password_hash) {
return Err(AuthError::InvalidCredentials);
}
Ok(AuthResult::Success { user })
}
```
Verify: `bun test` / `cargo test`
Commit: `feat: implement [feature] to pass tests`
Transition: Mark "Green" `completed`, create "Refactor" `in_progress`
</green_phase>
<refactor_phase>
Enhance code quality without changing behavior. Tests must continue passing.
Guidelines:
- Extract common patterns into well-named functions
- Apply SOLID principles where appropriate
- Improve types: discriminated unions, branded types
- No test behavior changes
- Run tests after each step
TypeScript:
```typescript
// Extract validation
function validateCredentials(creds: { email: string; password: string }): AuthResult | null {
if (!creds.password) return { type: 'error', code: 'MISSING_PASSWORD' }
if (!isValidEmail(creds.email)) return { type: 'error', code: 'INVALID_EMAIL' }
return null
}
// Branded types for safety
type Email = string & { readonly __brand: 'Email' }
```
Rust:
```rust
// Extract validation
fn validate_credentials(creds: &Credentials) -> Result<(), AuthError> {
if creds.password.is_empty() { return Err(AuthError::MissingPassword); }
if !is_valid_email(&creds.email) { return Err(AuthError::InvalidEmail); }
Ok(())
}
// Newtype for safety
pub struct Email(String);
```
Verify: `bun test` / `cargo test`
Commit: `refactor: [improvement description]`
Transition: Mark "Refactor" `completed`, create "Verify" `in_progress`
Final: Run full suite. Mark "Verify" `completed` when all checks pass.
</refactor_phase>
<organization>
Follow project conventions, defaulting to:
TypeScript/Bun:
```
src/{module}/{name}.ts # Implementation
src/{module}/{name}.test.ts # Unit tests colocated
src/{module}/__fixtures__/ # Test data
tests/integration/ # Integration tests
tests/e2e/ # End-to-end tests
```
Rust:
```
src/{module}/mod.rs # #[cfg(test)] mod tests { ... }
tests/integration/ # Integration tests
tests/fixtures/ # Test data
```
</organization>
<quality>
| Metric | Target |
|--------|--------|
| Line coverage | >=80% (90% critical paths) |
| Mutation score | >=75% |
| Unit test time | <5s |
Test characteristics:
- Single clear assertion per test
- No execution order dependencies
- Descriptive names forming sentences
- Behavior focus, not implementation
Smells to avoid:
- Setup longer than test
- Multiple unrelated assertions
- Coupling to implementation details
- Flaky tests
See [quality-metrics.md](references/quality-metrics.md) for coverage and mutation testing details.
</quality>
<bug_fixes>
TDD workflow for bugs:
1. Write failing test reproducing bug (Start "Red" `in_progress`)
2. Verify fails for right reason
3. Fix with minimal code (Transition to "Green")
4. Verify passes, all others still pass
5. Refactor if needed (Transition to "Refactor" or skip to "Verify")
6. Commit: `fix: [bug description] with test coverage`
Example:
```typescript
// 1. Failing test
test('handles division by zero gracefully', () => {
expect(divide(10, 0)).toMatchObject({ type: 'error', code: 'DIVISION_BY_ZERO' })
})
// 3. Fix
function divide(a: number, b: number): Result {
if (b === 0) return { type: 'error', code: 'DIVISION_BY_ZERO' }
return { type: 'success', value: a / b }
}
```
</bug_fixes>
<rules>
ALWAYS:
- Track progress with Tasks (load **maintain-tasks** skill)
- Write tests before implementation (RED first)
- Run tests after each stage
- Verify tests fail for right reason in RED
- Keep cycles 5-15 min max
- Descriptive test names forming sentences
- Test behavior, not implementation
- Each test = one reason to fail
NEVER:
- Skip to implementation without tests
- Change test behavior during refactoring
- Test implementation details or private methods
- Allow tests to depend on execution order
- Write flaky tests
- Mark stage complete without running tests
- Multiple unrelated assertions per test
</rules>
<quick_reference>
```bash
# TypeScript/Bun
bun test # Run all tests
bun test --watch # Watch mode
bun test --coverage # Coverage report
bun test --only # Run only .only tests
bun x stryker run # Mutation testing
# Rust
cargo test # Run all tests
cargo test --test NAME # Specific integration test
cargo tarpaulin # Coverage report
cargo mutants # Mutation testing
cargo test -- --nocapture # Show println! output
```
</quick_reference>
<references>
- [test-patterns.md](references/test-patterns.md) - Discriminated unions, builders, mocking, parameterized tests, async patterns for TypeScript and Rust
- [quality-metrics.md](references/quality-metrics.md) - Coverage analysis, mutation testing setup, CI integration, thresholds
- [feature-implementation.md](examples/feature-implementation.md) - Full TDD session walkthrough
- [bug-fix.md](examples/bug-fix.md) - TDD workflow for bug fixes
</references>
@@ -0,0 +1,420 @@
# TDD Session: Bug Fix
TDD workflow for fixing a production bug with test reproduction first.
## Bug Report
**Issue**: Division by zero crashes calculator service
**Reporter**: User reported 500 error when calculating percentage with zero total
**Stack Trace**:
```
Error: Division by zero
at calculatePercentage (calculator.ts:45)
at handler (api/stats.ts:23)
```
**Priority**: High — causes service crash
## TDD Workflow
### Task Setup
```text
1. Write failing test reproducing bug [in_progress]
2. Fix bug with minimal code [pending]
3. Refactor if needed [pending]
4. Verify fix [pending]
```
## RED Phase: Reproduce Bug (4 min)
Confidence: `▓▓░░░` → Can reproduce, know the fix
First, understand the current implementation:
**Read**: `src/calculator/percentage.ts`
```typescript
export function calculatePercentage(value: number, total: number): number {
return (value / total) * 100
}
```
Issue clear: No validation for zero total.
**Write failing test**: `src/calculator/percentage.test.ts`
```typescript
import { describe, test, expect } from 'bun:test'
import { calculatePercentage } from './percentage'
describe('calculatePercentage', () => {
// Existing tests
test('calculates percentage correctly', () => {
expect(calculatePercentage(25, 100)).toBe(25)
expect(calculatePercentage(50, 200)).toBe(25)
})
// NEW: Bug reproduction test
test('handles division by zero gracefully', () => {
const result = calculatePercentage(10, 0)
expect(result).toBe(0)
// OR throw error approach:
// expect(() => calculatePercentage(10, 0)).toThrow('Total cannot be zero')
})
test('handles zero value with valid total', () => {
expect(calculatePercentage(0, 100)).toBe(0)
})
})
```
**Run test**: `bun test percentage.test.ts`
```
calculatePercentage
✓ calculates percentage correctly
✗ handles division by zero gracefully
Expected: 0
Received: Infinity
✓ handles zero value with valid total
1 failing
```
Bug reproduced successfully — test fails as expected.
**Commit**: `test: add failing test for division by zero bug`
**Task Update**:
```text
1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [in_progress]
3. Refactor if needed [pending]
4. Verify fix [pending]
```
## GREEN Phase: Fix Bug (3 min)
Confidence: `▓▓▓▓░` → Know exact fix needed
**Update**: `src/calculator/percentage.ts`
```typescript
export function calculatePercentage(value: number, total: number): number {
// Fix: Handle division by zero
if (total === 0) {
return 0
}
return (value / total) * 100
}
```
**Run tests**: `bun test percentage.test.ts`
```
calculatePercentage
✓ calculates percentage correctly
✓ handles division by zero gracefully
✓ handles zero value with valid total
3 passing
```
All tests pass — bug fixed.
**Verify no regressions**: `bun test`
```
All test suites passing (23 tests total)
```
No other tests broken — safe fix.
**Commit**: `fix: handle division by zero in percentage calculation`
**Task Update**:
```text
1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [in_progress]
4. Verify fix [pending]
```
## REFACTOR Phase: Consider Improvements (5 min)
Current fix works but could be more robust. Consider:
1. Should negative totals be handled?
2. Should we return error instead of 0?
3. Type safety improvements?
**Decision**: Add input validation, use Result type for better error handling.
**Refactored**: `src/calculator/percentage.ts`
```typescript
type PercentageResult =
| { type: 'success'; value: number }
| { type: 'error'; code: 'ZERO_TOTAL' | 'NEGATIVE_TOTAL' }
export function calculatePercentage(
value: number,
total: number
): PercentageResult {
// Validate total
if (total === 0) {
return { type: 'error', code: 'ZERO_TOTAL' }
}
if (total < 0) {
return { type: 'error', code: 'NEGATIVE_TOTAL' }
}
return {
type: 'success',
value: (value / total) * 100,
}
}
```
**Update tests** to match new signature:
```typescript
describe('calculatePercentage', () => {
test('calculates percentage correctly', () => {
const result1 = calculatePercentage(25, 100)
const result2 = calculatePercentage(50, 200)
expect(result1).toEqual({ type: 'success', value: 25 })
expect(result2).toEqual({ type: 'success', value: 25 })
})
test('returns error for division by zero', () => {
const result = calculatePercentage(10, 0)
expect(result).toEqual({
type: 'error',
code: 'ZERO_TOTAL',
})
})
test('returns error for negative total', () => {
const result = calculatePercentage(10, -100)
expect(result).toEqual({
type: 'error',
code: 'NEGATIVE_TOTAL',
})
})
test('handles zero value with valid total', () => {
const result = calculatePercentage(0, 100)
expect(result).toEqual({ type: 'success', value: 0 })
})
})
```
**Run tests**: `bun test percentage.test.ts`
```
calculatePercentage
✓ calculates percentage correctly
✓ returns error for division by zero
✓ returns error for negative total
✓ handles zero value with valid total
4 passing
```
**Update usage** in `api/stats.ts`:
```typescript
// Before
const percentage = calculatePercentage(completed, total)
return { percentage }
// After
const result = calculatePercentage(completed, total)
if (result.type === 'error') {
return { error: 'Invalid calculation', code: result.code }
}
return { percentage: result.value }
```
**Run all tests**: `bun test`
```
All tests passing (24 tests total)
```
No regressions — refactor successful.
**Commit**: `refactor: use Result type for percentage calculation errors`
**Task Update**:
```text
1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [completed]
4. Verify fix [in_progress]
```
## VERIFY Phase: Comprehensive Validation (4 min)
Run full test suite with coverage:
```bash
bun test --coverage
```
```
Coverage Summary:
percentage.ts: 100% statements, 100% branches
All tests passing (24 total)
```
Run mutation testing on updated code:
```bash
bun x stryker run --mutate "src/calculator/percentage.ts"
```
```
Mutation Score: 100%
All mutants killed
Mutations tested:
✓ === → !== (killed by "returns error for division by zero")
✓ < → <= (killed by "returns error for negative total")
✓ / → * (killed by "calculates percentage correctly")
✓ 0 → 1 (killed by "handles zero value with valid total")
```
Perfect mutation score — tests thoroughly verify behavior.
**Integration test** to verify API fix:
```typescript
describe('GET /api/stats', () => {
test('handles zero total gracefully', async () => {
const response = await request(app)
.get('/api/stats')
.query({ completed: 10, total: 0 })
expect(response.status).toBe(200)
expect(response.body).toMatchObject({
error: 'Invalid calculation',
code: 'ZERO_TOTAL',
})
})
})
```
**Run integration tests**: `bun test tests/integration/`
```
API Integration Tests
✓ handles zero total gracefully
All integration tests passing
```
**Task Update**:
```text
1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [completed]
4. Verify fix [completed]
```
## Session Summary
**Duration**: 16 minutes total
- RED: 4 min
- GREEN: 3 min
- REFACTOR: 5 min
- VERIFY: 4 min
**Bug**: Division by zero crash
**Fix**: Added validation with Result type
**Tests**: 4 new tests + 1 integration test
**Coverage**: 100% on changed code
**Mutation Score**: 100%
**Improvements beyond minimal fix**:
- Used discriminated union for error handling
- Added negative total validation
- Updated API to handle error results
- Added integration test
**Production deployment**:
- All tests passing
- No regressions detected
- Error handling verified
- Ready to deploy
## Key TDD Bug Fix Principles
1. **RED first**: Always reproduce bug with failing test before fixing
2. **Minimal GREEN**: Fix the immediate issue first
3. **Refactor for robustness**: Improve error handling and edge cases
4. **Verify thoroughly**: Run full suite + mutation tests + integration tests
5. **Document in test**: Test name describes the bug being fixed
## Anti-patterns Avoided
Avoided jumping straight to fix without test:
```typescript
// ❌ Wrong approach
// 1. See bug report
// 2. Add if (total === 0) return 0
// 3. Deploy and hope
// ✓ Correct TDD approach
// 1. Write failing test reproducing bug
// 2. Verify test fails
// 3. Add minimal fix
// 4. Verify test passes
// 5. Refactor for robustness
// 6. Verify with mutation testing
```
Avoided over-engineering initial fix:
```typescript
// ❌ Too complex for first fix
if (total === 0 || total < 0 || !isFinite(total) || isNaN(total)) {
throw new ValidationError(...)
}
// ✓ Minimal fix first (GREEN phase)
if (total === 0) {
return 0
}
// ✓ Then refactor with proper error handling (REFACTOR phase)
if (total === 0) {
return { type: 'error', code: 'ZERO_TOTAL' }
}
```
## Commit History
```
test: add failing test for division by zero bug
fix: handle division by zero in percentage calculation
refactor: use Result type for percentage calculation errors
```
Clean, focused commits showing TDD progression.
@@ -0,0 +1,686 @@
# TDD Session: Feature Implementation
Complete TDD session implementing user authentication feature from scratch.
## Session Setup
**Feature**: User authentication with email/password
**Tech Stack**: TypeScript, Bun, discriminated unions for results
**Starting Point**: No existing code
**Duration**: ~45 minutes (3 RED-GREEN-REFACTOR cycles)
## Task State Tracking
Initial todos:
```text
1. Write failing test for user authentication [in_progress]
2. Implement authentication to pass tests [pending]
3. Refactor authentication code [pending]
4. Verify implementation [pending]
```
## Cycle 1: Basic Authentication
### RED Phase (5 min)
Starting confidence: `▓░░░░` → Writing tests to define interface
**Created**: `src/auth/authenticate.test.ts`
```typescript
import { describe, test, expect } from 'bun:test'
import { authenticate } from './authenticate'
describe('authenticate', () => {
const validCreds = {
email: 'user@example.com',
password: 'ValidPass123!',
} as const
test('returns success result with valid credentials', async () => {
const result = await authenticate(validCreds)
expect(result.type).toBe('success')
if (result.type === 'success') {
expect(result.user.email).toBe(validCreds.email)
}
})
test('returns error result with invalid credentials', async () => {
const result = await authenticate({
email: 'wrong@example.com',
password: 'wrong',
})
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('INVALID_CREDENTIALS')
}
})
test('returns error result with empty password', async () => {
const result = await authenticate({
email: 'user@example.com',
password: '',
})
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('MISSING_PASSWORD')
}
})
})
```
**Run tests**: `bun test`
```
authenticate
✗ returns success result with valid credentials
Error: Cannot find module "./authenticate"
✗ returns error result with invalid credentials
✗ returns error result with empty password
3 failing
```
Tests fail as expected — no implementation exists yet.
**Commit**: `test: add failing tests for user authentication`
**Task Update**:
```text
1. Write failing test for user authentication [completed]
2. Implement authentication to pass tests [in_progress]
3. Refactor authentication code [pending]
4. Verify implementation [pending]
```
### GREEN Phase (8 min)
Confidence: `▓▓░░░` → Implementing minimal solution
**Created**: `src/auth/authenticate.ts`
```typescript
type User = {
id: string
email: string
passwordHash: string
}
type AuthSuccess = {
type: 'success'
user: User
}
type AuthError = {
type: 'error'
code: 'INVALID_CREDENTIALS' | 'MISSING_PASSWORD'
}
type AuthResult = AuthSuccess | AuthError
// Minimal mock database
const users: User[] = [
{
id: '1',
email: 'user@example.com',
passwordHash: '$2a$10$hashedValidPass123!',
},
]
async function findUserByEmail(email: string): Promise<User | undefined> {
return users.find(u => u.email === email)
}
async function comparePassword(password: string, hash: string): Promise<boolean> {
// Simplified for testing - in production use bcrypt
return password === 'ValidPass123!' && hash === '$2a$10$hashedValidPass123!'
}
export async function authenticate(credentials: {
email: string
password: string
}): Promise<AuthResult> {
// Check password not empty
if (!credentials.password) {
return { type: 'error', code: 'MISSING_PASSWORD' }
}
// Find user
const user = await findUserByEmail(credentials.email)
if (!user) {
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// Verify password
const passwordMatch = await comparePassword(credentials.password, user.passwordHash)
if (!passwordMatch) {
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
return { type: 'success', user }
}
```
**Run tests**: `bun test`
```
authenticate
✓ returns success result with valid credentials
✓ returns error result with invalid credentials
✓ returns error result with empty password
3 passing
```
All tests pass — implementation complete.
**Commit**: `feat: implement user authentication to pass tests`
**Task Update**:
```text
1. Write failing test for user authentication [completed]
2. Implement authentication to pass tests [completed]
3. Refactor authentication code [in_progress]
4. Verify implementation [pending]
```
### REFACTOR Phase (7 min)
Confidence: `▓▓▓░░` → Improving structure and types
Observations:
- Types scattered throughout file
- Password comparison hardcoded
- No clear separation of concerns
**Refactored**: `src/auth/authenticate.ts`
```typescript
// Extract types to top
type User = {
id: string
email: string
passwordHash: string
}
type AuthSuccess = {
type: 'success'
user: User
}
type AuthError = {
type: 'error'
code: 'INVALID_CREDENTIALS' | 'MISSING_PASSWORD' | 'INVALID_EMAIL'
}
type AuthResult = AuthSuccess | AuthError
// Extract validation
function validateCredentials(credentials: {
email: string
password: string
}): AuthError | null {
if (!credentials.password) {
return { type: 'error', code: 'MISSING_PASSWORD' }
}
if (!credentials.email.includes('@')) {
return { type: 'error', code: 'INVALID_EMAIL' }
}
return null
}
// Mock database (unchanged)
const users: User[] = [
{
id: '1',
email: 'user@example.com',
passwordHash: '$2a$10$hashedValidPass123!',
},
]
async function findUserByEmail(email: string): Promise<User | undefined> {
return users.find(u => u.email === email)
}
// Extract password verification
async function verifyPassword(password: string, hash: string): Promise<boolean> {
// Simplified for testing - in production use bcrypt
return password === 'ValidPass123!' && hash === '$2a$10$hashedValidPass123!'
}
// Cleaner main function
export async function authenticate(credentials: {
email: string
password: string
}): Promise<AuthResult> {
// Validate input
const validationError = validateCredentials(credentials)
if (validationError) {
return validationError
}
// Find user
const user = await findUserByEmail(credentials.email)
if (!user) {
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// Verify password
const isValid = await verifyPassword(credentials.password, user.passwordHash)
if (!isValid) {
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
return { type: 'success', user }
}
```
**Run tests**: `bun test`
```
authenticate
✓ returns success result with valid credentials
✓ returns error result with invalid credentials
✓ returns error result with empty password
3 passing
```
Tests still pass — refactoring successful.
**Commit**: `refactor: extract validation and password verification`
**Task Update**:
```text
1. Write failing test for user authentication [completed]
2. Implement authentication to pass tests [completed]
3. Refactor authentication code [completed]
4. Verify implementation [in_progress]
```
### VERIFY Phase (3 min)
Run full test suite with coverage:
```bash
bun test --coverage
```
```
authenticate
✓ returns success result with valid credentials
✓ returns error result with invalid credentials
✓ returns error result with empty password
Coverage:
File | % Stmts | % Branch | % Funcs | % Lines
------------------|---------|----------|---------|--------
authenticate.ts | 95.45 | 100 | 100 | 95.45
3 passing
```
Coverage ≥80% — quality standards met.
**Task Update**:
```text
1. Write failing test for user authentication [completed]
2. Implement authentication to pass tests [completed]
3. Refactor authentication code [completed]
4. Verify implementation [completed]
```
## Cycle 2: Email Validation
Starting new cycle for email validation edge cases.
**Task Update**:
```text
1. Write failing test for email validation [in_progress]
2. Implement email validation to pass tests [pending]
3. Refactor email validation [pending]
4. Verify implementation [pending]
```
### RED Phase (4 min)
Add tests for email validation edge cases:
```typescript
describe('authenticate - email validation', () => {
test('returns error for invalid email format', async () => {
const result = await authenticate({
email: 'not-an-email',
password: 'ValidPass123!',
})
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('INVALID_EMAIL')
}
})
test('returns error for empty email', async () => {
const result = await authenticate({
email: '',
password: 'ValidPass123!',
})
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('INVALID_EMAIL')
}
})
})
```
**Run tests**: `bun test`
```
authenticate - email validation
✓ returns error for invalid email format # Already passes!
✗ returns error for empty email
Expected code: 'INVALID_EMAIL'
Received code: 'MISSING_PASSWORD'
1 failing
```
One test passes (basic email check exists), one fails.
**Commit**: `test: add email validation edge case tests`
### GREEN Phase (3 min)
Update validation to handle empty email:
```typescript
function validateCredentials(credentials: {
email: string
password: string
}): AuthError | null {
if (!credentials.email) {
return { type: 'error', code: 'INVALID_EMAIL' }
}
if (!credentials.password) {
return { type: 'error', code: 'MISSING_PASSWORD' }
}
if (!credentials.email.includes('@')) {
return { type: 'error', code: 'INVALID_EMAIL' }
}
return null
}
```
**Run tests**: `bun test`
```
authenticate - email validation
✓ returns error for invalid email format
✓ returns error for empty email
All tests passing (5 total)
```
**Commit**: `feat: validate empty email addresses`
### REFACTOR Phase (4 min)
Extract email validation to dedicated function:
```typescript
function isValidEmail(email: string): boolean {
return email.length > 0 && email.includes('@')
}
function validateCredentials(credentials: {
email: string
password: string
}): AuthError | null {
if (!isValidEmail(credentials.email)) {
return { type: 'error', code: 'INVALID_EMAIL' }
}
if (!credentials.password) {
return { type: 'error', code: 'MISSING_PASSWORD' }
}
return null
}
```
**Run tests**: `bun test` — All passing
**Commit**: `refactor: extract email validation function`
### VERIFY Phase (2 min)
```bash
bun test --coverage
```
Coverage: 96.2% — excellent.
## Cycle 3: Rate Limiting
Implementing rate limiting for failed authentication attempts.
### RED Phase (6 min)
Add tests for rate limiting:
```typescript
describe('authenticate - rate limiting', () => {
test('allows authentication after successful login', async () => {
const validCreds = {
email: 'user@example.com',
password: 'ValidPass123!',
}
const result1 = await authenticate(validCreds)
const result2 = await authenticate(validCreds)
expect(result1.type).toBe('success')
expect(result2.type).toBe('success')
})
test('blocks authentication after 3 failed attempts', async () => {
const invalidCreds = {
email: 'user@example.com',
password: 'wrong',
}
// 3 failed attempts
await authenticate(invalidCreds)
await authenticate(invalidCreds)
await authenticate(invalidCreds)
// 4th attempt should be rate limited
const result = await authenticate(invalidCreds)
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('RATE_LIMITED')
}
})
})
```
**Run tests**: `bun test` — Rate limit tests fail as expected
**Commit**: `test: add rate limiting tests`
### GREEN Phase (10 min)
Implement basic rate limiting:
```typescript
type AuthError = {
type: 'error'
code: 'INVALID_CREDENTIALS' | 'MISSING_PASSWORD' | 'INVALID_EMAIL' | 'RATE_LIMITED'
}
// Track failed attempts
const failedAttempts = new Map<string, number>()
function incrementFailedAttempts(email: string): void {
const current = failedAttempts.get(email) || 0
failedAttempts.set(email, current + 1)
}
function resetFailedAttempts(email: string): void {
failedAttempts.delete(email)
}
function isRateLimited(email: string): boolean {
const attempts = failedAttempts.get(email) || 0
return attempts >= 3
}
export async function authenticate(credentials: {
email: string
password: string
}): Promise<AuthResult> {
// Check rate limiting first
if (isRateLimited(credentials.email)) {
return { type: 'error', code: 'RATE_LIMITED' }
}
// Validate input
const validationError = validateCredentials(credentials)
if (validationError) {
return validationError
}
// Find user
const user = await findUserByEmail(credentials.email)
if (!user) {
incrementFailedAttempts(credentials.email)
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// Verify password
const isValid = await verifyPassword(credentials.password, user.passwordHash)
if (!isValid) {
incrementFailedAttempts(credentials.email)
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// Reset on success
resetFailedAttempts(credentials.email)
return { type: 'success', user }
}
```
**Run tests**: `bun test` — All 7 tests passing
**Commit**: `feat: implement rate limiting for failed authentication`
### REFACTOR Phase (6 min)
Extract rate limiting to separate module for testability:
**Created**: `src/auth/rate-limiter.ts`
```typescript
export class RateLimiter {
private attempts = new Map<string, number>()
constructor(private maxAttempts: number = 3) {}
increment(key: string): void {
const current = this.attempts.get(key) || 0
this.attempts.set(key, current + 1)
}
reset(key: string): void {
this.attempts.delete(key)
}
isLimited(key: string): boolean {
const attempts = this.attempts.get(key) || 0
return attempts >= this.maxAttempts
}
}
```
Update `authenticate.ts` to use class:
```typescript
import { RateLimiter } from './rate-limiter'
const rateLimiter = new RateLimiter(3)
export async function authenticate(credentials: {
email: string
password: string
}): Promise<AuthResult> {
// Check rate limiting first
if (rateLimiter.isLimited(credentials.email)) {
return { type: 'error', code: 'RATE_LIMITED' }
}
// ... rest unchanged ...
// On failure
if (!isValid) {
rateLimiter.increment(credentials.email)
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// On success
rateLimiter.reset(credentials.email)
return { type: 'success', user }
}
```
**Run tests**: `bun test` — All passing
**Commit**: `refactor: extract rate limiter to separate class`
### VERIFY Phase (5 min)
Final verification with mutation testing:
```bash
bun test --coverage
bun x stryker run
```
Results:
- Coverage: 94.8%
- Mutation score: 78.3%
- All tests passing
**Task**: All completed
## Session Summary
Duration: 45 minutes
Cycles: 3 complete RED-GREEN-REFACTOR cycles
Tests: 7 tests, all passing
Coverage: 94.8% line coverage
Mutation: 78.3% mutation score
Features implemented:
1. Basic authentication with email/password
2. Email validation
3. Rate limiting for failed attempts
Code quality:
- All types explicit
- Functions single-purpose
- Tests cover happy path and edge cases
- Mutation testing verifies test quality
Next steps:
- Add integration tests with real database
- Implement actual bcrypt password hashing
- Add time-based rate limit expiration
@@ -0,0 +1,587 @@
# Test Quality Metrics
Comprehensive guide to measuring and improving test quality through coverage and mutation testing.
## Coverage Metrics
### Line Coverage
Percentage of code lines executed during test runs.
**Target**: ≥80% overall, ≥90% for critical paths
**TypeScript/Bun**:
```bash
bun test --coverage
# Output
Coverage Summary:
Statements : 85.2% ( 1420/1667 )
Branches : 78.5% ( 314/400 )
Functions : 82.1% ( 156/190 )
Lines : 85.2% ( 1420/1667 )
```
**Rust**:
```bash
# Using cargo-tarpaulin
cargo tarpaulin --out Html --output-dir coverage/
# Using cargo-llvm-cov
cargo llvm-cov --html
```
### Branch Coverage
Percentage of decision branches (if/else, switch, ternary) executed.
**Target**: ≥75%
Example showing uncovered branch:
```typescript
function divide(a: number, b: number): number {
if (b === 0) { // Branch covered
throw new Error('Division by zero')
}
return a / b // Branch covered
}
// Test only covers success path
test('divides numbers', () => {
expect(divide(10, 2)).toBe(5)
})
// Coverage: 50% branches (only success branch covered)
```
Fix with both branches:
```typescript
test('divides numbers', () => {
expect(divide(10, 2)).toBe(5)
})
test('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero')
})
// Coverage: 100% branches
```
### Function Coverage
Percentage of functions called during tests.
**Target**: ≥80%
Uncovered functions often indicate:
- Dead code that should be removed
- Missing test cases
- Helper functions only used in uncovered paths
### Interpreting Coverage
High coverage ≠ high quality. Coverage shows what's tested, not how well.
**Example of misleading coverage**:
```typescript
function processPayment(amount: number): Result {
if (amount <= 0) {
return { type: 'error', code: 'INVALID_AMOUNT' }
}
const result = chargeCard(amount)
return { type: 'success', transactionId: result.id }
}
// Bad test with 100% coverage
test('processes payment', () => {
processPayment(100)
processPayment(-10)
})
// No assertions! 100% coverage but 0% verification
```
Coverage shows code was executed, not that it was verified correct.
## Mutation Testing
Mutation testing verifies test quality by introducing small bugs and checking if tests catch them.
### How It Works
1. **Mutant Generation**: Tool mutates source code (e.g., `===``!==`, `+``-`)
2. **Test Execution**: Run tests against each mutant
3. **Classification**:
- **Killed**: Test fails (good — test caught the bug)
- **Survived**: Test passes (bad — test missed the bug)
- **Timeout**: Mutant caused infinite loop
- **No Coverage**: Line not executed by tests
### Mutation Score
```
Mutation Score = (Killed Mutants / Total Mutants) × 100%
```
**Target**: ≥75%
### TypeScript Mutation Testing
Using Stryker:
**Install**:
```bash
bun add -d @stryker-mutator/core @stryker-mutator/typescript-checker
```
**Configuration** (`stryker.conf.json`):
```json
{
"mutator": "typescript",
"packageManager": "bun",
"reporters": ["html", "clear-text", "progress"],
"testRunner": "bun",
"coverageAnalysis": "perTest",
"mutate": [
"src/**/*.ts",
"!src/**/*.test.ts",
"!src/**/*.spec.ts"
],
"thresholds": {
"high": 80,
"low": 60,
"break": 50
}
}
```
**Run**:
```bash
bun x stryker run
# Output
Mutation testing complete:
Killed: 78
Survived: 12
Timeout: 2
No Coverage: 8
Mutation Score: 78.0%
```
**Common Mutations**:
| Original | Mutant | Catches |
|----------|--------|---------|
| `===` | `!==` | Equality assertions |
| `>` | `>=` | Boundary tests |
| `+` | `-` | Arithmetic verification |
| `&&` | `||` | Logic tests |
| `true` | `false` | Boolean verification |
| `0` | `1` | Zero handling |
| `return x` | `return undefined` | Return value tests |
**Example Analysis**:
```typescript
function calculateDiscount(price: number, isPremium: boolean): number {
if (isPremium) {
return price * 0.8 // 20% discount
}
return price
}
// Weak test
test('calculates discount', () => {
calculateDiscount(100, true)
calculateDiscount(100, false)
})
// Mutation: 0.8 → 0.9
// Status: Survived (no assertion)
```
Fix with assertions:
```typescript
test('applies 20% discount for premium users', () => {
expect(calculateDiscount(100, true)).toBe(80)
})
test('no discount for regular users', () => {
expect(calculateDiscount(100, false)).toBe(100)
})
// Mutation: 0.8 → 0.9
// Status: Killed (test fails with 90 !== 80)
```
### Rust Mutation Testing
Using `cargo-mutants`:
**Install**:
```bash
cargo install cargo-mutants
```
**Run**:
```bash
cargo mutants
# Output
Mutation testing results:
caught: 45
missed: 5
timeout: 1
unviable: 2
score: 90.0%
```
**Common Mutations**:
| Original | Mutant | Catches |
|----------|--------|---------|
| `==` | `!=` | Equality tests |
| `>` | `>=` | Boundary tests |
| `&&` | `||` | Logic tests |
| `Some(x)` | `None` | Option handling |
| `Ok(x)` | `Err(...)` | Result handling |
| `+` | `-` | Arithmetic verification |
**Example**:
```rust
fn calculate_discount(price: i32, is_premium: bool) -> i32 {
if is_premium {
price * 80 / 100 // 20% discount
} else {
price
}
}
// Weak test
#[test]
fn test_discount() {
calculate_discount(100, true);
calculate_discount(100, false);
}
// Mutation: 80 → 90
// Status: missed (no assertion)
```
Fix:
```rust
#[test]
fn applies_discount_for_premium() {
assert_eq!(calculate_discount(100, true), 80);
}
#[test]
fn no_discount_for_regular() {
assert_eq!(calculate_discount(100, false), 100);
}
// Mutation: 80 → 90
// Status: caught (assertion fails)
```
## Quality Standards Matrix
| Metric | Minimum | Good | Excellent |
|--------|---------|------|-----------|
| Line Coverage | 70% | 80% | 90% |
| Branch Coverage | 65% | 75% | 85% |
| Function Coverage | 75% | 85% | 95% |
| Mutation Score | 60% | 75% | 85% |
| Test Execution Time | <10s | <5s | <2s |
## Improving Test Quality
### Weak Assertion Detection
**Problem**: Tests execute code but don't verify results
```typescript
// ❌ Weak - no verification
test('processes order', () => {
processOrder({ items: [item1, item2] })
})
```
**Solution**:
```typescript
// ✓ Strong - verifies result
test('processes order', () => {
const result = processOrder({ items: [item1, item2] })
expect(result.type).toBe('success')
expect(result.total).toBe(150)
})
```
### Missing Edge Cases
Use mutation testing to find gaps:
```typescript
function validateAge(age: number): boolean {
return age >= 18 // Mutant: >= → >
}
// Current test
test('validates age', () => {
expect(validateAge(20)).toBe(true)
expect(validateAge(16)).toBe(false)
})
// Mutation survived: >= → >
// Missing: boundary test for exactly 18
```
Add boundary test:
```typescript
test('accepts exactly 18', () => {
expect(validateAge(18)).toBe(true)
})
// Now mutation is caught
```
### Test Redundancy
Multiple tests verifying same thing:
```typescript
// Redundant tests
test('validates positive number', () => {
expect(isPositive(5)).toBe(true)
})
test('validates another positive number', () => {
expect(isPositive(10)).toBe(true)
})
test('validates yet another positive number', () => {
expect(isPositive(100)).toBe(true)
})
```
Consolidate:
```typescript
test.each([5, 10, 100])('validates positive number %i', (num) => {
expect(isPositive(num)).toBe(true)
})
```
## Continuous Quality Monitoring
### CI/CD Integration
**TypeScript**:
```yaml
# .github/workflows/test.yml
- name: Run tests with coverage
run: bun test --coverage
- name: Check coverage thresholds
run: |
coverage=$(bun test --coverage --json | jq '.coverage.total.statements.pct')
if (( $(echo "$coverage < 80" | bc -l) )); then
echo "Coverage $coverage% below 80% threshold"
exit 1
fi
- name: Run mutation testing
run: bun x stryker run
# Fail if mutation score < 75%
```
**Rust**:
```yaml
# .github/workflows/test.yml
- name: Run tests with coverage
run: cargo tarpaulin --fail-under 80
- name: Run mutation testing
run: cargo mutants
continue-on-error: true # Warning only initially
```
### Tracking Over Time
Monitor trends:
```bash
# Generate coverage badge
coverage=$(bun test --coverage --json | jq '.coverage.total.statements.pct')
echo "Coverage: $coverage%" > coverage.txt
# Track mutation score
mutation=$(bun x stryker run --json | jq '.mutationScore')
echo "Mutation Score: $mutation%" > mutation.txt
```
## Advanced Techniques
### Differential Coverage
Only measure coverage on changed code:
```bash
# Get changed files
git diff --name-only main... > changed.txt
# Run coverage on changed files
bun test --coverage --changed-files changed.txt
```
### Coverage Ratcheting
Prevent coverage from decreasing:
```bash
# Save current coverage
current=$(bun test --coverage --json | jq '.coverage.total.statements.pct')
echo "$current" > .baseline-coverage
# On future runs, compare
baseline=$(cat .baseline-coverage)
if (( $(echo "$current < $baseline" | bc -l) )); then
echo "Coverage decreased from $baseline% to $current%"
exit 1
fi
```
### Mutation Testing Optimization
Run only on changed code:
```bash
# Stryker incremental mode
bun x stryker run --incremental
# cargo-mutants on specific files
cargo mutants --file src/auth/mod.rs
```
## Common Pitfalls
### Pitfall 1: Chasing 100% Coverage
**Problem**: Diminishing returns past 90%, testing trivial code
```typescript
// Trivial getter - not worth testing
class User {
get email(): string {
return this._email
}
}
```
**Solution**: Focus on behavior, not line count. Exclude trivial code from coverage requirements.
### Pitfall 2: Gaming Metrics
**Problem**: Tests that execute code without verification
```typescript
// ❌ High coverage, zero value
test('calls all functions', () => {
func1()
func2()
func3()
})
```
**Solution**: Use mutation testing to catch weak assertions.
### Pitfall 3: Slow Mutation Testing
**Problem**: Full mutation testing takes hours
**Solution**: Run incrementally or in CI only:
```bash
# Local: Quick feedback on changed files
bun x stryker run --mutate "src/auth/**/*.ts"
# CI: Full suite
bun x stryker run
```
## Quality Metrics Dashboard
Example report format:
```
Test Quality Report
===================
Coverage:
Statements: 85.2% ░░░░░░░░▓▓
Branches: 78.5% ░░░░░░░▓▓▓
Functions: 82.1% ░░░░░░░░▓▓
Mutation Testing:
Score: 78.0% ░░░░░░░▓▓▓
Killed: 78
Survived: 12
No Cov: 8
Performance:
Unit Tests: 2.3s ✓
Total: 8.7s ✓
Status: ✓ All thresholds met
```
## Actionable Improvement Plan
1. **Week 1**: Establish baseline
- Run coverage analysis
- Run mutation testing
- Document current state
2. **Week 2-3**: Fix critical gaps
- Add tests for uncovered critical paths
- Fix survived mutants in high-risk code
- Target 80% coverage, 75% mutation score
3. **Week 4**: Automate
- Add CI coverage checks
- Set up coverage ratcheting
- Schedule weekly mutation testing
4. **Ongoing**: Maintain
- Review coverage on each PR
- Run mutation testing monthly
- Gradually raise thresholds
## Resources
TypeScript:
- [Stryker Documentation](https://stryker-mutator.io)
- [Bun Test Coverage](https://bun.sh/docs/cli/test#coverage)
Rust:
- [cargo-tarpaulin](https://github.com/xd009642/tarpaulin)
- [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov)
- [cargo-mutants](https://github.com/sourcefrog/cargo-mutants)
@@ -0,0 +1,804 @@
# Test Patterns Reference
Comprehensive test patterns for TypeScript/Bun and Rust.
## TypeScript/Bun Patterns
### Basic Test Structure
```typescript
import { describe, test, expect } from 'bun:test'
describe('Module or Feature Name', () => {
test('describes specific behavior', () => {
// Arrange
const input = createTestInput()
// Act
const result = functionUnderTest(input)
// Assert
expect(result).toBe(expected)
})
})
```
### Discriminated Unions for Test Scenarios
Use discriminated unions to make test scenarios type-safe:
```typescript
type TestScenario =
| { type: 'success'; input: ValidInput; expected: Output }
| { type: 'error'; input: InvalidInput; expectedError: ErrorCode }
| { type: 'edge-case'; input: EdgeInput; expected: Output }
test.each<TestScenario>([
{
type: 'success',
input: { value: 100 },
expected: { result: 100 },
},
{
type: 'error',
input: { value: -1 },
expectedError: 'NEGATIVE_VALUE',
},
{
type: 'edge-case',
input: { value: 0 },
expected: { result: 0 },
},
])('handles $type scenario', async (scenario) => {
const result = await processValue(scenario.input)
if (scenario.type === 'success' || scenario.type === 'edge-case') {
expect(result).toEqual(scenario.expected)
} else {
expect(result.error).toBe(scenario.expectedError)
}
})
```
### Type-Safe Test Builders
Create fluent builders for complex test data:
```typescript
class UserBuilder {
private data: Partial<User> = {
id: 'test-id',
email: 'test@example.com',
role: 'user',
createdAt: new Date('2024-01-01'),
}
withId(id: string): this {
this.data.id = id
return this
}
withEmail(email: string): this {
this.data.email = email
return this
}
withRole(role: UserRole): this {
this.data.role = role
return this
}
asAdmin(): this {
return this.withRole('admin')
}
build(): User {
return this.data as User
}
}
// Usage
const adminUser = new UserBuilder()
.withEmail('admin@example.com')
.asAdmin()
.build()
```
Generic builder for flexibility:
```typescript
class Builder<T> {
constructor(private defaults: T) {}
with<K extends keyof T>(key: K, value: T[K]): this {
this.defaults = { ...this.defaults, [key]: value }
return this
}
build(): T {
return { ...this.defaults }
}
}
// Usage
const userBuilder = new Builder<User>({
id: 'test-id',
email: 'test@example.com',
role: 'user',
})
const admin = userBuilder.with('role', 'admin').build()
```
### Const Assertions for Test Data
Type-safe test data with const assertions:
```typescript
const validInputs = [
{ input: 'hello', expected: 'HELLO' },
{ input: 'world', expected: 'WORLD' },
{ input: '', expected: '' },
] as const
test.each(validInputs)(
'transforms $input to $expected',
({ input, expected }) => {
expect(transform(input)).toBe(expected)
}
)
```
### Async Testing Patterns
Promise rejection:
```typescript
test('rejects with error for invalid input', async () => {
const promise = fetchUser('invalid-id')
await expect(promise).rejects.toThrow(UserNotFoundError)
await expect(promise).rejects.toThrow('User not found')
})
```
Async/await with error handling:
```typescript
test('handles async errors gracefully', async () => {
const result = await processData('invalid').catch(err => ({
error: err.message,
}))
expect(result.error).toBe('Invalid data')
})
```
Timeout handling:
```typescript
test('times out slow operations', async () => {
const promise = slowOperation()
await expect(
Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 100)
),
])
).rejects.toThrow('Timeout')
})
```
### Mocking with Bun
Module mocking:
```typescript
import { mock } from 'bun:test'
// Mock entire module
mock.module('./database', () => ({
query: mock(() => Promise.resolve({ rows: [] })),
connect: mock(() => Promise.resolve()),
}))
// Use in test
test('handles database errors', async () => {
const { query } = await import('./database')
query.mockImplementationOnce(() => Promise.reject(new Error('DB Error')))
const result = await fetchUsers()
expect(result.error).toBe('DB Error')
})
```
Function mocking:
```typescript
const mockFetch = mock(async (url: string) => ({
ok: true,
json: async () => ({ data: 'test' }),
}))
test('fetches data successfully', async () => {
const result = await fetchData('https://api.example.com', mockFetch)
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com')
expect(result.data).toBe('test')
})
```
### Snapshot Testing
Simple snapshots:
```typescript
test('serializes user correctly', () => {
const user = new UserBuilder().build()
expect(JSON.stringify(user, null, 2)).toMatchSnapshot()
})
```
Inline snapshots:
```typescript
test('formats error message', () => {
const error = new ValidationError('Invalid email')
expect(error.message).toMatchInlineSnapshot(`"Invalid email"`)
})
```
### Parameterized Tests
Basic parameterization:
```typescript
test.each([
[1, 1],
[2, 4],
[3, 9],
[4, 16],
])('square(%i) returns %i', (input, expected) => {
expect(square(input)).toBe(expected)
})
```
Object-based parameterization:
```typescript
test.each([
{ input: 5, expected: 25, description: 'positive number' },
{ input: -3, expected: 9, description: 'negative number' },
{ input: 0, expected: 0, description: 'zero' },
])('square($input) for $description', ({ input, expected }) => {
expect(square(input)).toBe(expected)
})
```
### Focused Testing
Run specific tests:
```typescript
// Only run this test
test.only('current feature under development', () => {
// Fast feedback during active development
})
// Skip slow tests during TDD
test.skip('slow integration test', () => {
// Run in CI but not during rapid TDD cycles
})
// Mark test as work in progress
test.todo('implement rate limiting')
```
### Parallel Test Execution
Run independent tests in parallel:
```typescript
describe.concurrent('Independent Operations', () => {
test('operation 1', async () => {
const result = await independentOp1()
expect(result).toBeDefined()
})
test('operation 2', async () => {
const result = await independentOp2()
expect(result).toBeDefined()
})
test('operation 3', async () => {
const result = await independentOp3()
expect(result).toBeDefined()
})
})
```
### Error Testing Patterns
Exception testing:
```typescript
test('throws error for invalid input', () => {
expect(() => processData(null)).toThrow(ValidationError)
expect(() => processData(null)).toThrow('Input cannot be null')
})
```
Error result testing:
```typescript
test('returns error result for invalid input', () => {
const result = processData(null)
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('INVALID_INPUT')
expect(result.message).toContain('null')
}
})
```
## Rust Patterns
### Basic Test Structure
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_functionality() {
// Arrange
let input = setup_test_input();
// Act
let result = function_under_test(input);
// Assert
assert_eq!(result, expected);
}
}
```
### Property-Based Testing
Using `proptest`:
```rust
use proptest::prelude::*;
proptest! {
#[test]
fn password_hash_is_deterministic(password in "[a-zA-Z0-9]{8,32}") {
let hash1 = hash_password(&password);
let hash2 = hash_password(&password);
prop_assert_eq!(hash1, hash2);
}
#[test]
fn email_validation_never_panics(email in ".*") {
let result = validate_email(&email);
// Should always return Ok or Err, never panic
prop_assert!(result.is_ok() || result.is_err());
}
#[test]
fn parse_then_serialize_is_identity(value in 0..1000) {
let serialized = serialize_value(value);
let parsed = parse_value(&serialized).unwrap();
prop_assert_eq!(value, parsed);
}
}
```
### Test Builders
Rust builder pattern:
```rust
#[derive(Default)]
struct UserBuilder {
id: Option<String>,
email: Option<String>,
role: Option<Role>,
}
impl UserBuilder {
fn new() -> Self {
Self::default()
}
fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
fn with_role(mut self, role: Role) -> Self {
self.role = Some(role);
self
}
fn as_admin(self) -> Self {
self.with_role(Role::Admin)
}
fn build(self) -> User {
User {
id: self.id.unwrap_or_else(|| "test-id".to_string()),
email: self.email.unwrap_or_else(|| "test@example.com".to_string()),
role: self.role.unwrap_or(Role::User),
}
}
}
// Usage
#[test]
fn test_admin_permissions() {
let admin = UserBuilder::new()
.with_email("admin@example.com")
.as_admin()
.build();
assert!(has_admin_access(&admin));
}
```
### Async Testing
Using `tokio::test`:
```rust
#[tokio::test]
async fn authenticates_user_async() {
let credentials = Credentials {
email: "user@example.com".to_string(),
password: "password".to_string(),
};
let result = authenticate_async(&credentials).await;
assert!(result.is_ok());
}
#[tokio::test]
#[should_panic(expected = "timeout")]
async fn times_out_slow_operations() {
tokio::time::timeout(
Duration::from_millis(100),
very_slow_operation()
).await.expect("timeout");
}
```
### Result Testing
Testing `Result` types:
```rust
#[test]
fn returns_error_for_invalid_input() {
let result = process_data(None);
assert!(result.is_err());
assert!(matches!(result, Err(ProcessError::InvalidInput)));
}
#[test]
fn returns_success_for_valid_input() {
let result = process_data(Some("valid"));
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value, "processed");
}
```
Using `assert_matches!` macro:
```rust
#[test]
fn authenticates_with_valid_credentials() {
let result = authenticate(&valid_creds);
assert!(matches!(result, Ok(AuthResult::Success { .. })));
}
#[test]
fn rejects_invalid_credentials() {
let result = authenticate(&invalid_creds);
assert!(matches!(result, Err(AuthError::InvalidCredentials)));
}
```
### Documentation Tests
Executable documentation:
```rust
/// Authenticates a user with credentials.
///
/// # Examples
///
/// ```
/// use auth::{authenticate, Credentials};
///
/// let creds = Credentials {
/// email: "user@example.com".to_string(),
/// password: "password".to_string(),
/// };
///
/// let result = authenticate(&creds);
/// assert!(result.is_ok());
/// ```
///
/// # Errors
///
/// Returns `AuthError::InvalidCredentials` if credentials are invalid:
///
/// ```
/// use auth::{authenticate, Credentials, AuthError};
///
/// let bad_creds = Credentials {
/// email: "wrong@example.com".to_string(),
/// password: "wrong".to_string(),
/// };
///
/// let result = authenticate(&bad_creds);
/// assert!(matches!(result, Err(AuthError::InvalidCredentials)));
/// ```
pub fn authenticate(credentials: &Credentials) -> Result<AuthResult, AuthError> {
// Implementation
}
```
### Snapshot Testing
Using `insta`:
```rust
use insta::assert_snapshot;
#[test]
fn serializes_user_correctly() {
let user = User {
id: "test-id".to_string(),
email: "test@example.com".to_string(),
role: Role::Admin,
};
let serialized = serde_json::to_string_pretty(&user).unwrap();
assert_snapshot!(serialized);
}
```
### Parameterized Tests
Manual parameterization:
```rust
#[test]
fn test_square() {
let test_cases = vec![
(5, 25),
(-3, 9),
(0, 0),
(10, 100),
];
for (input, expected) in test_cases {
assert_eq!(square(input), expected, "Failed for input {}", input);
}
}
```
Using `rstest`:
```rust
use rstest::rstest;
#[rstest]
#[case(5, 25)]
#[case(-3, 9)]
#[case(0, 0)]
#[case(10, 100)]
fn test_square(#[case] input: i32, #[case] expected: i32) {
assert_eq!(square(input), expected);
}
```
### Mock Objects
Using `mockall`:
```rust
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::*;
use mockall::mock;
mock! {
Database {}
impl Database {
fn query(&self, sql: &str) -> Result<Vec<Row>, DbError>;
fn execute(&self, sql: &str) -> Result<u64, DbError>;
}
}
#[test]
fn handles_empty_database() {
let mut mock_db = MockDatabase::new();
mock_db
.expect_query()
.with(eq("SELECT * FROM users"))
.returning(|_| Ok(vec![]));
let users = find_all_users(&mock_db);
assert_eq!(users.len(), 0);
}
#[test]
fn handles_database_error() {
let mut mock_db = MockDatabase::new();
mock_db
.expect_query()
.returning(|_| Err(DbError::ConnectionLost));
let result = find_all_users(&mock_db);
assert!(result.is_err());
}
}
```
### Error Testing
Custom error types:
```rust
#[test]
fn returns_custom_error() {
let result = process_value(-1);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.to_string(), "Value cannot be negative");
}
#[test]
fn error_contains_context() {
let result = parse_config("invalid");
match result {
Err(ConfigError::ParseError { line, message }) => {
assert_eq!(line, 1);
assert!(message.contains("invalid"));
}
_ => panic!("Expected ParseError"),
}
}
```
### Integration Test Structure
Separate integration tests in `tests/` directory:
```rust
// tests/integration/user_api.rs
use my_crate::*;
#[tokio::test]
async fn test_user_registration_flow() {
// Setup test database
let db = setup_test_db().await;
// Create user
let user = register_user(&db, "test@example.com", "password").await.unwrap();
// Verify user created
let found = find_user(&db, user.id).await.unwrap();
assert_eq!(found.email, "test@example.com");
// Cleanup
cleanup_test_db(db).await;
}
```
## Common Test Smells and Solutions
### Test Smell: Setup Longer Than Test
❌ Bad:
```typescript
test('processes order', () => {
const user = { id: '1', email: 'test@example.com', role: 'user', /* 10 more fields */ }
const product = { id: 'p1', name: 'Widget', price: 100, /* 8 more fields */ }
const cart = { items: [{ product, quantity: 2 }], /* 5 more fields */ }
const payment = { method: 'card', /* 6 more fields */ }
const result = processOrder(user, cart, payment)
expect(result.total).toBe(200)
})
```
✓ Good:
```typescript
test('processes order', () => {
const order = new OrderBuilder().withQuantity(2).withPrice(100).build()
const result = processOrder(order)
expect(result.total).toBe(200)
})
```
### Test Smell: Multiple Unrelated Assertions
❌ Bad:
```typescript
test('user management', () => {
expect(createUser('test@example.com')).toBeDefined()
expect(findUser('1')).toEqual({ id: '1' })
expect(deleteUser('1')).toBe(true)
})
```
✓ Good:
```typescript
test('creates user with valid email', () => {
expect(createUser('test@example.com')).toBeDefined()
})
test('finds user by id', () => {
expect(findUser('1')).toEqual({ id: '1' })
})
test('deletes user successfully', () => {
expect(deleteUser('1')).toBe(true)
})
```
### Test Smell: Testing Implementation Details
❌ Bad:
```typescript
test('caches results internally', () => {
const service = new UserService()
service.fetchUser('1')
expect(service._cache.has('1')).toBe(true) // Testing private implementation
})
```
✓ Good:
```typescript
test('returns cached user on second fetch', async () => {
const service = new UserService()
const spy = mock.fn()
await service.fetchUser('1', spy)
await service.fetchUser('1', spy)
expect(spy).toHaveBeenCalledTimes(1) // Testing observable behavior
})
```