📦 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,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
})
```