Unit Testing
Purpose
Write effective unit tests using FIRST principles, AAA pattern, test doubles, and TDD. Ensure test quality, maintainability, and meaningful coverage of business logic. This skill covers test architecture, isolation strategies, mock/stub decisions, CI integration, and test suite health monitoring.
Agent Protocol
Trigger
User mentions unit testing, test doubles, mocking, stubbing, TDD, FIRST principles, code coverage, test structure (AAA), or unit test patterns.
Input Context
- Modules or functions under test
- Dependency graph and injection points
- Existing test suite structure
- Coverage targets and CI constraints
- Language and test framework in use
Output Artifact
Unit test suite with proper isolation, test doubles, and CI integration.
Response Format
Test file(s) with:
- Proper AAA structure and descriptive naming
- Mock/stub/fake definitions at system boundaries
- Coverage configuration and CI pipeline integration
- Test organization strategy (co-located or centralized)
Completion Criteria
- All identified functions have unit tests covering happy path, error paths, and edge cases
- Test doubles used only at system boundaries
- Tests are fast (< 100ms per test, < 5 min for suite)
- Coverage meets agreed thresholds (line > 80%, branch > 70%)
- CI integration configured with coverage reporting and sharding
Workflow
- Analyze the unit: Identify the function/module, its dependencies, and its observable behavior (not implementation)
- Determine isolation needs: Map dependencies to test double types (mock at boundaries, real for pure functions)
- Design test scenarios: List happy path, error paths, edge cases (empty, null, boundary values)
- Create test structure: Write describe blocks for organization, it blocks per scenario following AAA/Given-When-Then
- Implement mocks: Configure mock responses at dependency boundaries. Use vi.mock/jest.mock for module mocks, vi.spyOn for method spies
- Write assertions: Assert observable behavior (return values, state changes, side effects). Avoid asserting internal implementation
- Run and verify: Execute tests. Verify coverage meets thresholds. Check test execution time
- Refactor tests: Improve test code quality, remove duplication, extract test helpers and factories
- Integrate CI: Configure CI with parallel execution, sharding, coverage reporting, and quality gates
- Monitor suite health: Track execution time, flakiness, coverage stability. Fix flaky tests immediately
Architecture / Decision Trees
Test Double Selection
Dependency type?
├── External HTTP/network → Mock (MSW/WireMock)
├── Database → Fake (in-memory repo) or Mock at repository boundary
├── File system → Mock (memfs) or in-memory implementation
├── Time (Date, setTimeout) → Fake timers (vi.useFakeTimers)
├── Random/UUID → Stub with fixed values
├── Logger → Stub (no-op or spy)
├── Pure function → Real implementation
└── Internal service → Real implementation or Spy
Framework Selection
Project language?
├── TypeScript/JavaScript
│ ├── New project + ESM → Vitest
│ └── Existing Jest → Jest
├── Java → JUnit 5 + Mockito
├── Python → pytest + pytest-mock
├── Go → testing + testify
├── Rust → cargo test
└── C# → xUnit + Moq/NSubstitute
Common Pitfalls
- Over-mocking: Mocking internal details creates brittle tests that break on refactoring. Mock only at module/system boundaries
- Testing implementation, not behavior: Tests that assert internal method calls or private state break during refactoring
- Shared mutable state across tests: Tests that depend on earlier tests' side effects are unreliable and order-dependent
- Slow tests: Tests using real databases, network calls, or filesystem are integration tests, not unit tests
- Mocking dependencies you don't own: Mocking third-party library internals creates tight coupling to library details
- Empty assertions: Tests without proper assertions (expect(true).toBe(true)) provide false confidence
- Brittle string matching: Asserting exact error messages or rendered output makes tests fragile
- Granularity mismatch: Testing too large (entire service) or too small (private helpers) a unit
- Skipping error paths: Only testing the happy path misses the majority of potential bugs
- Coverage chasing: Targeting 100% coverage encourages meaningless tests. Focus on business logic
Best Practices
- Follow the AAA pattern (Arrange-Act-Assert) consistently across all tests
- Use descriptive test names: "should [expected] when [scenario]"
- Mock at system boundaries (network, persistence, time), not implementation internals
- Use real implementations for pure functions and value objects
- Keep tests fast: mock I/O, use in-memory dependencies, avoid real timers
- One assertion concept per test — multiple assertions are fine if they test one behavior
- Use factory functions for test data creation with sensible defaults and overrides
- Clean up after tests: restore mocks, reset timers, clear state
- Write tests alongside or before production code (TDD) for complex logic
- Run tests in watch mode during development for fast feedback
Compared With
| Aspect |
Unit Testing |
Integration Testing |
E2E Testing |
| Scope |
Single function/module |
Component interactions |
Full user workflows |
| Isolation |
Full (mocked deps) |
Partial (real deps) |
None (real system) |
| Speed |
Milliseconds |
Seconds |
Minutes |
| Debugging |
Easy (isolated) |
Medium |
Hard |
| Brittleness |
Low |
Medium |
High |
| Confidence |
Low (isolated) |
Medium |
High |
| When |
Every commit |
On feature completion |
Pre-release |
Performance Considerations
- Individual tests should complete in < 100ms; suites under 5 minutes
- Mock I/O to reduce test time from seconds to microseconds
- Use test sharding in CI to parallelize across workers
- Configure
mockReset and restoreMocks for automatic cleanup
- Use
it.concurrent for independent tests within the same describe block
- Avoid beforeEach/afterEach for expensive setup — use beforeAll/afterAll for shared resources
- Watch mode skips unchanged files for fast feedback during development
Unit Test Examples
TypeScript/Vitest — Service with Dependency Injection
// src/services/order.service.ts
export class OrderService {
constructor(
private orderRepo: OrderRepository,
private paymentGateway: PaymentGateway,
private emailService: EmailService,
) {}
async placeOrder(cart: Cart, customerId: string): Promise<Order> {
const total = cart.items.reduce((sum, item) => sum + item.price, 0);
const payment = await this.paymentGateway.charge(customerId, total);
const order = await this.orderRepo.create({ customerId, total, paymentId: payment.id });
await this.emailService.sendOrderConfirmation(customerId, order.id);
return order;
}
}
// src/services/__tests__/order.service.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { OrderService } from "../order.service";
describe("OrderService", () => {
let service: OrderService;
let mockOrderRepo: OrderRepository;
let mockPaymentGateway: PaymentGateway;
let mockEmailService: EmailService;
beforeEach(() => {
mockOrderRepo = { create: vi.fn() };
mockPaymentGateway = { charge: vi.fn() };
mockEmailService = { sendOrderConfirmation: vi.fn() };
service = new OrderService(mockOrderRepo, mockPaymentGateway, mockEmailService);
});
it("should create order and charge payment when placing order", async () => {
const cart = { items: [{ price: 50 }, { price: 30 }] };
mockPaymentGateway.charge.mockResolvedValue({ id: "pay_123" });
mockOrderRepo.create.mockResolvedValue({ id: "ord_456" });
const result = await service.placeOrder(cart, "cust_789");
expect(mockPaymentGateway.charge).toHaveBeenCalledWith("cust_789", 80);
expect(mockOrderRepo.create).toHaveBeenCalledWith({
customerId: "cust_789",
total: 80,
paymentId: "pay_123",
});
expect(mockEmailService.sendOrderConfirmation).toHaveBeenCalledWith("cust_789", "ord_456");
expect(result).toEqual({ id: "ord_456" });
});
it("should not create order when payment fails", async () => {
const cart = { items: [{ price: 50 }] };
mockPaymentGateway.charge.mockRejectedValue(new Error("insufficient_funds"));
await expect(service.placeOrder(cart, "cust_789")).rejects.toThrow("insufficient_funds");
expect(mockOrderRepo.create).not.toHaveBeenCalled();
expect(mockEmailService.sendOrderConfirmation).not.toHaveBeenCalled();
});
});
Python/pytest — Pure Function Testing
# src/pricing.py
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class PriceBreak:
quantity: int
discount_percent: Decimal
def calculate_discount(quantity: int, breaks: list[PriceBreak]) -> Decimal:
applicable = [b for b in breaks if quantity >= b.quantity]
if not applicable:
return Decimal("0")
return max(applicable, key=lambda b: b.discount_percent).discount_percent
def calculate_total(items: list[dict], discount_pct: Decimal) -> Decimal:
subtotal = sum(Decimal(str(item["price"])) * item["quantity"] for item in items)
discount = subtotal * (discount_pct / Decimal("100"))
return subtotal - discount
# tests/test_pricing.py
import pytest
from decimal import Decimal
from src.pricing import calculate_discount, calculate_total, PriceBreak
class TestCalculateDiscount:
def test_no_breaks_returns_zero(self):
assert calculate_discount(10, []) == Decimal("0")
def test_quantity_meets_single_break(self):
breaks = [PriceBreak(5, Decimal("10"))]
assert calculate_discount(5, breaks) == Decimal("10")
def test_quantity_exceeds_break(self):
breaks = [PriceBreak(5, Decimal("10"))]
assert calculate_discount(10, breaks) == Decimal("10")
def test_highest_applicable_break_wins(self):
breaks = [
PriceBreak(5, Decimal("10")),
PriceBreak(10, Decimal("15")),
PriceBreak(20, Decimal("20")),
]
assert calculate_discount(15, breaks) == Decimal("15")
assert calculate_discount(25, breaks) == Decimal("20")
def test_quantity_below_all_breaks_returns_zero(self):
breaks = [PriceBreak(5, Decimal("10"))]
assert calculate_discount(3, breaks) == Decimal("0")
Python/pytest — Mocking External Service
# tests/test_order_service.py
from unittest.mock import Mock, patch
from src.order_service import OrderService
class TestOrderService:
@patch("src.order_service.EmailService")
@patch("src.order_service.PaymentGateway")
@patch("src.order_service.OrderRepository")
def test_place_order_success(
self, mock_repo_cls, mock_payment_cls, mock_email_cls
):
mock_repo = Mock()
mock_payment = Mock()
mock_email = Mock()
mock_repo_cls.return_value = mock_repo
mock_payment_cls.return_value = mock_payment
mock_email_cls.return_value = mock_email
service = OrderService()
mock_payment.charge.return_value = {"id": "pay_123"}
mock_repo.create.return_value = {"id": "ord_456"}
cart = {"items": [{"price": "50.00", "quantity": 1}]}
result = service.place_order(cart, "cust_789")
assert result["id"] == "ord_456"
mock_payment.charge.assert_called_once()
mock_repo.create.assert_called_once()
mock_email.send_confirmation.assert_called_once()
CI Integration for Unit Tests
GitHub Actions — Unit Test Stage
name: Unit Tests
on: pull_request
jobs:
unit:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx vitest run --reporter=junit --shard=${{ matrix.shard }}/4
- uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-${{ matrix.shard }}
path: coverage/
coverage:
needs: unit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx vitest run --coverage
- uses: davelosert/vitest-coverage-report-action@v2
with:
json-summary-path: coverage/coverage-summary.json
Unit Testing Anti-Patterns
Anti-Pattern: Over-Mocking
Mocking every dependency including pure functions and internal collaborators creates brittle tests that break during refactoring. Only mock at system boundaries (network, filesystem, time, RNG). Use real implementations for value objects, pure functions, and internal helpers.
Anti-Pattern: Testing Private Methods
Testing private methods directly couples tests to implementation details. Private methods change during refactoring while public behavior stays the same. Test through the public interface only. If a private method has complex logic, extract it to a separate module and test it as a public function.
Anti-Pattern: Shared Mutable State
Tests that share mutable state through module-level variables or test fixtures with side effects produce order-dependent failures. Each test must be independently runnable. Use beforeEach to create fresh instances. Never modify shared state in a test.
Anti-Pattern: Asserting Implementation Details
Asserting that a specific method was called internally or that a specific property exists on an object. These assertions break during refactoring. Assert only on observable behavior: return values, thrown exceptions, and side effects visible to the caller.
Anti-Pattern: Empty or Trivial Tests
Tests that call a function but don't assert anything, or assert tautologies like expect(true).toBe(true). Every test must assert at least one meaningful outcome. If a test passes without any real assertion, it provides false confidence.
Anti-Pattern: Ignoring Edge Cases
Testing only the happy path (correct input, expected behavior) misses null/undefined inputs, boundary values, empty collections, and error conditions. Every function should have tests for: normal input, boundary values, invalid input, empty/null input, and error conditions.
Unit Testing Maturity Model
| Level |
Characteristics |
Practices |
| 1: Initial |
No or minimal unit tests |
Manual testing only, no test framework, no CI integration |
| 2: Defined |
Basic coverage for critical logic |
Tests for core business logic, basic mock usage, some CI integration |
| 3: Managed |
Systematic unit testing |
AAA pattern, proper test doubles at boundaries, >70% coverage, CI gates, test naming conventions |
| 4: Measured |
Test-driven quality culture |
TDD for complex logic, property-based testing supplement, <100ms per test, flakiness < 0.1% |
| 5: Optimized |
Self-healing test suite |
Mutation testing for quality validation, AI-assisted test generation, automatic test maintenance, coverage-based risk assessment |
Performance Considerations
- Individual test execution: target < 100ms. Tests slower than 100ms need investigation.
- Suite execution: target < 5 minutes for full unit suite. Use sharding for parallel execution in CI.
- Mock setup overhead: vi.mock/jest.mock adds 1-5ms per mocked module. Minimize module-level mocks.
- Test data creation: factory functions should complete in < 10ms. Avoid database writes in factories.
- Coverage computation: adds 2x-3x execution time. Run with coverage only in CI, not in watch mode.
- File watcher: < 500ms reload time for watch mode. Large projects may need test file filters.
Rules
- Every test must follow AAA or Given-When-Then structure — no unstructured test bodies
- Mock only at system boundaries (network, persistence, filesystem, time). Internal dependencies use real implementations
- Tests must not share mutable state — each test must be independently runnable and order-independent
- Test names must describe behavior, not implementation:
should return sorted array when called with unsorted input
- Each test must assert at least one observable outcome — no assertions-only-in-catch or empty tests
- Coverage thresholds: line >= 80%, branch >= 70% for business logic code. Exclude generated code
- No test may make real network calls, access real databases, or write to the real filesystem outside temp
- Time-dependent code must use fake timers (vi.useFakeTimers) for deterministic execution
- All mocks must be reset between tests (use mockReset: true in config or afterEach restoreAllMocks)
- Skipped tests (it.skip) must not be committed — use test.todo for planned tests instead
- At least one negative/error test must accompany every positive/happy path test
- Test data must use factories or fixtures with sensible defaults — no copy-pasted test data
- PRs must not decrease coverage below configured thresholds without documented exception
- Flaky tests (non-deterministic failures) must be quarantined within 24 hours and fixed within one sprint
- Private methods are tested only through public interface — never expose privates for testing
- Test files must be co-located with source files for unit tests, centralized for integration/E2E
- Watch mode defaults to changed files only for fast feedback during development
- Console.log assertions in tests must be removed before committing — use spy on console
References
- references/mocking-strategies.md — Mocking Strategies
- references/tdd-guide.md — TDD Guide
- references/test-doubles.md — Test Doubles Guide
- references/test-organization.md — Test Organization
- references/test-patterns.md — Unit Test Patterns
- references/unit-testing-advanced.md — Unit Testing Advanced Topics
- references/unit-testing-architecture.md — Unit Testing Architecture and System Design
- references/unit-testing-fundamentals.md — Unit Testing Fundamentals
- references/unit-testing-patterns.md — Unit Testing Patterns
- references/unit-testing-workflow-strategies.md — Unit Testing Workflow Strategies and Decision Frameworks
Handoff
After unit testing, hand off to:
quality-integration-testing — for verifying component interactions with real dependencies
quality-property-based-testing — for adding property-based invariants to complement examples
quality-regression-testing — for regression suite execution and maintenance
quality-smoke-testing — for BVT smoke test definition on tested components
Implementation Patterns
Observer Pattern for Event Handling
`
interface EventObserver {
onEvent(event: T): Promise;
}
class EventBus {
private observers: Set<EventObserver> = new Set();
subscribe(observer: EventObserver): void {
this.observers.add(observer);
}
unsubscribe(observer: EventObserver): void {
this.observers.delete(observer);
}
async emit(event: T): Promise {
const results = Array.from(this.observers).map(o => o.onEvent(event));
await Promise.allSettled(results);
}
}
`
Configuration-Driven Approach
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
Production Considerations
Deployment Checklist
Monitoring and Alerting
| Metric |
Threshold |
Severity |
Action |
| Error rate |
> 1% over 5min |
Critical |
Page on-call |
| p99 latency |
> 2s over 5min |
Warning |
Investigate |
| Throughput drop |
> 50% over 1min |
Critical |
Check upstream |
| Queue depth |
> 1000 over 1min |
Warning |
Scale consumers |
| Disk usage |
> 85% |
Warning |
Clean or expand |
| Memory usage |
> 90% heap |
Critical |
Restart or scale |
Anti-Patterns
| Anti-Pattern |
Symptom |
Root Cause |
Solution |
| Premature optimization |
Complex code for no measured benefit |
Guessing instead of profiling |
Measure first, optimize based on data |
| Copy-paste reuse |
Duplicate code across codebase |
Lack of abstraction |
Extract shared logic into libraries |
| Gold-plating |
Features with no current requirement |
Over-engineering |
YAGNI — build what's needed now |
| Magical thinking |
Assumptions without validation |
Skipping error handling |
Handle all failure modes explicitly |
Performance Optimization
Caching Strategy
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge).
Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
Resource Pooling
- Database connections: Pool of reusable connections (HikariCP, pgBouncer)
- HTTP connections: Keep-alive + connection pooling for external calls
- Thread pool: Bounded thread pools for async task execution
Profiling Methodology
- Establish baseline with production traffic profile
- Profile CPU with sampling profiler (pprof, perf, async-profiler)
- Profile memory with heap dumps and allocation tracking
- Profile I/O with strace/perf trace for syscall analysis
- Profile latency with distributed tracing (OpenTelemetry)
- Identify bottleneck, formulate hypothesis, implement fix
- Re-profile to verify improvement, repeat
Security Considerations
Threat Modeling (STRIDE)
- Spoofing: Identity validation, authentication
- Tampering: Integrity checks, digital signatures
- Repudiation: Audit logs, non-repudiation
- Information disclosure: Encryption, access control
- Denial of service: Rate limiting, resource quotas
- Elevation of privilege: Principle of least privilege
Supply Chain Security
- Dependency scanning: Snyk, Dependabot, Trivy
- SBOM generation: CycloneDX or SPDX format
- Signed commits: GPG or SSH commit signing
- Artifact verification: Checksum validation, signature verification
Secrets Management
- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)
- Rotation policy: Rotate database credentials every 90 days
- Access audit: Log every secrets access, alert on anomalies
- Encryption at rest and in transit for all secrets
- Principle of least privilege: each service gets only its own secrets
Rules
- Default-deny security posture — allow only explicitly required access.
- All inputs validated, all outputs encoded, all errors handled.
- Defend in depth — multiple layers of security controls.
- Fail securely — errors default to safe behavior.
- Log security-relevant events for audit and investigation.
- Keep dependencies updated — automate vulnerability scanning.
- Design for observability from day one, not as an afterthought.
- Document all architectural decisions with rationale.
- Review code for security, performance, and correctness before merging.
Architecture Decision Trees
Unit Test Approach
| Decision Point |
Option A |
Option B |
Decision Criteria |
| Test framework |
Jest (JS/TS ecosystem) |
Vitest (faster, ESM-native) |
Framework maturity, ESM compatibility |
| Assertion style |
expect(x).toBe(y) (readable) |
assert.equal(x, y) (minimal) |
Team preference, existing codebase |
| Mocking style |
Jest mocks (built-in) |
ts-mockito/typed-mock |
Type safety, TypeScript usage |
| Coverage tool |
Built-in (Jest Istanbul) |
c8 (modern, ESM-friendly) |
Coverage needs, configuration complexity |
What to Unit Test
- Business logic → Pure functions, complex algorithms
- Data transformations → Mappers, serializers, validators
- Edge cases → Empty states, boundary values, error conditions
- NOT trivial → Simple getters/setters, framework wiring
1---2name: quality-unit-testing3description: Use when the user asks about unit testing, test doubles, mocking, stubbing, test-driven development (TDD), FIRST principles, code coverage, test structure (AAA), or unit test patterns. Do NOT use for: integration testing (quality-integration-testing), E2E testing (quality-e2e-testing), or frontend testing (frontend-testing).4license: MIT5---67# Unit Testing89## Purpose10Write effective unit tests using FIRST principles, AAA pattern, test doubles, and TDD. Ensure test quality, maintainability, and meaningful coverage of business logic. This skill covers test architecture, isolation strategies, mock/stub decisions, CI integration, and test suite health monitoring.1112## Agent Protocol1314### Trigger15User mentions unit testing, test doubles, mocking, stubbing, TDD, FIRST principles, code coverage, test structure (AAA), or unit test patterns.1617### Input Context18- Modules or functions under test19- Dependency graph and injection points20- Existing test suite structure21- Coverage targets and CI constraints22- Language and test framework in use2324### Output Artifact25Unit test suite with proper isolation, test doubles, and CI integration.2627### Response Format28Test file(s) with:291. Proper AAA structure and descriptive naming302. Mock/stub/fake definitions at system boundaries313. Coverage configuration and CI pipeline integration324. Test organization strategy (co-located or centralized)3334### Completion Criteria35- All identified functions have unit tests covering happy path, error paths, and edge cases36- Test doubles used only at system boundaries37- Tests are fast (< 100ms per test, < 5 min for suite)38- Coverage meets agreed thresholds (line > 80%, branch > 70%)39- CI integration configured with coverage reporting and sharding4041## Workflow42431. **Analyze the unit**: Identify the function/module, its dependencies, and its observable behavior (not implementation)442. **Determine isolation needs**: Map dependencies to test double types (mock at boundaries, real for pure functions)453. **Design test scenarios**: List happy path, error paths, edge cases (empty, null, boundary values)464. **Create test structure**: Write describe blocks for organization, it blocks per scenario following AAA/Given-When-Then475. **Implement mocks**: Configure mock responses at dependency boundaries. Use vi.mock/jest.mock for module mocks, vi.spyOn for method spies486. **Write assertions**: Assert observable behavior (return values, state changes, side effects). Avoid asserting internal implementation497. **Run and verify**: Execute tests. Verify coverage meets thresholds. Check test execution time508. **Refactor tests**: Improve test code quality, remove duplication, extract test helpers and factories519. **Integrate CI**: Configure CI with parallel execution, sharding, coverage reporting, and quality gates5210. **Monitor suite health**: Track execution time, flakiness, coverage stability. Fix flaky tests immediately5354## Architecture / Decision Trees5556### Test Double Selection5758```59Dependency type?60├── External HTTP/network → Mock (MSW/WireMock)61├── Database → Fake (in-memory repo) or Mock at repository boundary62├── File system → Mock (memfs) or in-memory implementation63├── Time (Date, setTimeout) → Fake timers (vi.useFakeTimers)64├── Random/UUID → Stub with fixed values65├── Logger → Stub (no-op or spy)66├── Pure function → Real implementation67└── Internal service → Real implementation or Spy68```6970### Framework Selection7172```73Project language?74├── TypeScript/JavaScript75│ ├── New project + ESM → Vitest76│ └── Existing Jest → Jest77├── Java → JUnit 5 + Mockito78├── Python → pytest + pytest-mock79├── Go → testing + testify80├── Rust → cargo test81└── C# → xUnit + Moq/NSubstitute82```8384## Common Pitfalls85861. **Over-mocking**: Mocking internal details creates brittle tests that break on refactoring. Mock only at module/system boundaries872. **Testing implementation, not behavior**: Tests that assert internal method calls or private state break during refactoring883. **Shared mutable state across tests**: Tests that depend on earlier tests' side effects are unreliable and order-dependent894. **Slow tests**: Tests using real databases, network calls, or filesystem are integration tests, not unit tests905. **Mocking dependencies you don't own**: Mocking third-party library internals creates tight coupling to library details916. **Empty assertions**: Tests without proper assertions (expect(true).toBe(true)) provide false confidence927. **Brittle string matching**: Asserting exact error messages or rendered output makes tests fragile938. **Granularity mismatch**: Testing too large (entire service) or too small (private helpers) a unit949. **Skipping error paths**: Only testing the happy path misses the majority of potential bugs9510. **Coverage chasing**: Targeting 100% coverage encourages meaningless tests. Focus on business logic9697## Best Practices98991. Follow the AAA pattern (Arrange-Act-Assert) consistently across all tests1002. Use descriptive test names: "should [expected] when [scenario]"1013. Mock at system boundaries (network, persistence, time), not implementation internals1024. Use real implementations for pure functions and value objects1035. Keep tests fast: mock I/O, use in-memory dependencies, avoid real timers1046. One assertion concept per test — multiple assertions are fine if they test one behavior1057. Use factory functions for test data creation with sensible defaults and overrides1068. Clean up after tests: restore mocks, reset timers, clear state1079. Write tests alongside or before production code (TDD) for complex logic10810. Run tests in watch mode during development for fast feedback109110## Compared With111112| Aspect | Unit Testing | Integration Testing | E2E Testing |113|--------|-------------|-------------------|-------------|114| Scope | Single function/module | Component interactions | Full user workflows |115| Isolation | Full (mocked deps) | Partial (real deps) | None (real system) |116| Speed | Milliseconds | Seconds | Minutes |117| Debugging | Easy (isolated) | Medium | Hard |118| Brittleness | Low | Medium | High |119| Confidence | Low (isolated) | Medium | High |120| When | Every commit | On feature completion | Pre-release |121122## Performance Considerations123124- Individual tests should complete in < 100ms; suites under 5 minutes125- Mock I/O to reduce test time from seconds to microseconds126- Use test sharding in CI to parallelize across workers127- Configure `mockReset` and `restoreMocks` for automatic cleanup128- Use `it.concurrent` for independent tests within the same describe block129- Avoid beforeEach/afterEach for expensive setup — use beforeAll/afterAll for shared resources130- Watch mode skips unchanged files for fast feedback during development131132## Unit Test Examples133134### TypeScript/Vitest — Service with Dependency Injection135```typescript136// src/services/order.service.ts137export class OrderService {138 constructor(139 private orderRepo: OrderRepository,140 private paymentGateway: PaymentGateway,141 private emailService: EmailService,142 ) {}143144 async placeOrder(cart: Cart, customerId: string): Promise<Order> {145 const total = cart.items.reduce((sum, item) => sum + item.price, 0);146 const payment = await this.paymentGateway.charge(customerId, total);147 const order = await this.orderRepo.create({ customerId, total, paymentId: payment.id });148 await this.emailService.sendOrderConfirmation(customerId, order.id);149 return order;150 }151}152153// src/services/__tests__/order.service.test.ts154import { describe, it, expect, vi, beforeEach } from "vitest";155import { OrderService } from "../order.service";156157describe("OrderService", () => {158 let service: OrderService;159 let mockOrderRepo: OrderRepository;160 let mockPaymentGateway: PaymentGateway;161 let mockEmailService: EmailService;162163 beforeEach(() => {164 mockOrderRepo = { create: vi.fn() };165 mockPaymentGateway = { charge: vi.fn() };166 mockEmailService = { sendOrderConfirmation: vi.fn() };167 service = new OrderService(mockOrderRepo, mockPaymentGateway, mockEmailService);168 });169170 it("should create order and charge payment when placing order", async () => {171 const cart = { items: [{ price: 50 }, { price: 30 }] };172 mockPaymentGateway.charge.mockResolvedValue({ id: "pay_123" });173 mockOrderRepo.create.mockResolvedValue({ id: "ord_456" });174175 const result = await service.placeOrder(cart, "cust_789");176177 expect(mockPaymentGateway.charge).toHaveBeenCalledWith("cust_789", 80);178 expect(mockOrderRepo.create).toHaveBeenCalledWith({179 customerId: "cust_789",180 total: 80,181 paymentId: "pay_123",182 });183 expect(mockEmailService.sendOrderConfirmation).toHaveBeenCalledWith("cust_789", "ord_456");184 expect(result).toEqual({ id: "ord_456" });185 });186187 it("should not create order when payment fails", async () => {188 const cart = { items: [{ price: 50 }] };189 mockPaymentGateway.charge.mockRejectedValue(new Error("insufficient_funds"));190191 await expect(service.placeOrder(cart, "cust_789")).rejects.toThrow("insufficient_funds");192 expect(mockOrderRepo.create).not.toHaveBeenCalled();193 expect(mockEmailService.sendOrderConfirmation).not.toHaveBeenCalled();194 });195});196```197198### Python/pytest — Pure Function Testing199```python200# src/pricing.py201from dataclasses import dataclass202from decimal import Decimal203204@dataclass205class PriceBreak:206 quantity: int207 discount_percent: Decimal208209def calculate_discount(quantity: int, breaks: list[PriceBreak]) -> Decimal:210 applicable = [b for b in breaks if quantity >= b.quantity]211 if not applicable:212 return Decimal("0")213 return max(applicable, key=lambda b: b.discount_percent).discount_percent214215def calculate_total(items: list[dict], discount_pct: Decimal) -> Decimal:216 subtotal = sum(Decimal(str(item["price"])) * item["quantity"] for item in items)217 discount = subtotal * (discount_pct / Decimal("100"))218 return subtotal - discount219220# tests/test_pricing.py221import pytest222from decimal import Decimal223from src.pricing import calculate_discount, calculate_total, PriceBreak224225class TestCalculateDiscount:226 def test_no_breaks_returns_zero(self):227 assert calculate_discount(10, []) == Decimal("0")228229 def test_quantity_meets_single_break(self):230 breaks = [PriceBreak(5, Decimal("10"))]231 assert calculate_discount(5, breaks) == Decimal("10")232233 def test_quantity_exceeds_break(self):234 breaks = [PriceBreak(5, Decimal("10"))]235 assert calculate_discount(10, breaks) == Decimal("10")236237 def test_highest_applicable_break_wins(self):238 breaks = [239 PriceBreak(5, Decimal("10")),240 PriceBreak(10, Decimal("15")),241 PriceBreak(20, Decimal("20")),242 ]243 assert calculate_discount(15, breaks) == Decimal("15")244 assert calculate_discount(25, breaks) == Decimal("20")245246 def test_quantity_below_all_breaks_returns_zero(self):247 breaks = [PriceBreak(5, Decimal("10"))]248 assert calculate_discount(3, breaks) == Decimal("0")249```250251### Python/pytest — Mocking External Service252```python253# tests/test_order_service.py254from unittest.mock import Mock, patch255from src.order_service import OrderService256257class TestOrderService:258 @patch("src.order_service.EmailService")259 @patch("src.order_service.PaymentGateway")260 @patch("src.order_service.OrderRepository")261 def test_place_order_success(262 self, mock_repo_cls, mock_payment_cls, mock_email_cls263 ):264 mock_repo = Mock()265 mock_payment = Mock()266 mock_email = Mock()267 mock_repo_cls.return_value = mock_repo268 mock_payment_cls.return_value = mock_payment269 mock_email_cls.return_value = mock_email270271 service = OrderService()272 mock_payment.charge.return_value = {"id": "pay_123"}273 mock_repo.create.return_value = {"id": "ord_456"}274275 cart = {"items": [{"price": "50.00", "quantity": 1}]}276 result = service.place_order(cart, "cust_789")277278 assert result["id"] == "ord_456"279 mock_payment.charge.assert_called_once()280 mock_repo.create.assert_called_once()281 mock_email.send_confirmation.assert_called_once()282```283284## CI Integration for Unit Tests285286### GitHub Actions — Unit Test Stage287```yaml288name: Unit Tests289on: pull_request290jobs:291 unit:292 runs-on: ubuntu-latest293 strategy:294 matrix:295 shard: [1, 2, 3, 4]296 steps:297 - uses: actions/checkout@v4298 - uses: actions/setup-node@v4299 with:300 node-version: 20301 - run: npm ci302 - run: npx vitest run --reporter=junit --shard=${{ matrix.shard }}/4303 - uses: actions/upload-artifact@v4304 if: always()305 with:306 name: coverage-${{ matrix.shard }}307 path: coverage/308 coverage:309 needs: unit310 runs-on: ubuntu-latest311 steps:312 - uses: actions/checkout@v4313 - run: npm ci314 - run: npx vitest run --coverage315 - uses: davelosert/vitest-coverage-report-action@v2316 with:317 json-summary-path: coverage/coverage-summary.json318```319320## Unit Testing Anti-Patterns321322### Anti-Pattern: Over-Mocking323Mocking every dependency including pure functions and internal collaborators creates brittle tests that break during refactoring. Only mock at system boundaries (network, filesystem, time, RNG). Use real implementations for value objects, pure functions, and internal helpers.324325### Anti-Pattern: Testing Private Methods326Testing private methods directly couples tests to implementation details. Private methods change during refactoring while public behavior stays the same. Test through the public interface only. If a private method has complex logic, extract it to a separate module and test it as a public function.327328### Anti-Pattern: Shared Mutable State329Tests that share mutable state through module-level variables or test fixtures with side effects produce order-dependent failures. Each test must be independently runnable. Use `beforeEach` to create fresh instances. Never modify shared state in a test.330331### Anti-Pattern: Asserting Implementation Details332Asserting that a specific method was called internally or that a specific property exists on an object. These assertions break during refactoring. Assert only on observable behavior: return values, thrown exceptions, and side effects visible to the caller.333334### Anti-Pattern: Empty or Trivial Tests335Tests that call a function but don't assert anything, or assert tautologies like `expect(true).toBe(true)`. Every test must assert at least one meaningful outcome. If a test passes without any real assertion, it provides false confidence.336337### Anti-Pattern: Ignoring Edge Cases338Testing only the happy path (correct input, expected behavior) misses null/undefined inputs, boundary values, empty collections, and error conditions. Every function should have tests for: normal input, boundary values, invalid input, empty/null input, and error conditions.339340## Unit Testing Maturity Model341342| Level | Characteristics | Practices |343|---|---|---|344| 1: Initial | No or minimal unit tests | Manual testing only, no test framework, no CI integration |345| 2: Defined | Basic coverage for critical logic | Tests for core business logic, basic mock usage, some CI integration |346| 3: Managed | Systematic unit testing | AAA pattern, proper test doubles at boundaries, >70% coverage, CI gates, test naming conventions |347| 4: Measured | Test-driven quality culture | TDD for complex logic, property-based testing supplement, <100ms per test, flakiness < 0.1% |348| 5: Optimized | Self-healing test suite | Mutation testing for quality validation, AI-assisted test generation, automatic test maintenance, coverage-based risk assessment |349350## Performance Considerations351352- Individual test execution: target < 100ms. Tests slower than 100ms need investigation.353- Suite execution: target < 5 minutes for full unit suite. Use sharding for parallel execution in CI.354- Mock setup overhead: vi.mock/jest.mock adds 1-5ms per mocked module. Minimize module-level mocks.355- Test data creation: factory functions should complete in < 10ms. Avoid database writes in factories.356- Coverage computation: adds 2x-3x execution time. Run with coverage only in CI, not in watch mode.357- File watcher: < 500ms reload time for watch mode. Large projects may need test file filters.358359## Rules3601. Every test must follow AAA or Given-When-Then structure — no unstructured test bodies3612. Mock only at system boundaries (network, persistence, filesystem, time). Internal dependencies use real implementations3623. Tests must not share mutable state — each test must be independently runnable and order-independent3634. Test names must describe behavior, not implementation: `should return sorted array when called with unsorted input`3645. Each test must assert at least one observable outcome — no assertions-only-in-catch or empty tests3656. Coverage thresholds: line >= 80%, branch >= 70% for business logic code. Exclude generated code3667. No test may make real network calls, access real databases, or write to the real filesystem outside temp3678. Time-dependent code must use fake timers (vi.useFakeTimers) for deterministic execution3689. All mocks must be reset between tests (use mockReset: true in config or afterEach restoreAllMocks)36910. Skipped tests (it.skip) must not be committed — use test.todo for planned tests instead37011. At least one negative/error test must accompany every positive/happy path test37112. Test data must use factories or fixtures with sensible defaults — no copy-pasted test data37213. PRs must not decrease coverage below configured thresholds without documented exception37314. Flaky tests (non-deterministic failures) must be quarantined within 24 hours and fixed within one sprint37415. Private methods are tested only through public interface — never expose privates for testing37516. Test files must be co-located with source files for unit tests, centralized for integration/E2E37617. Watch mode defaults to changed files only for fast feedback during development37718. Console.log assertions in tests must be removed before committing — use spy on console378379## References380- references/mocking-strategies.md — Mocking Strategies381- references/tdd-guide.md — TDD Guide382- references/test-doubles.md — Test Doubles Guide383- references/test-organization.md — Test Organization384- references/test-patterns.md — Unit Test Patterns385- references/unit-testing-advanced.md — Unit Testing Advanced Topics386- references/unit-testing-architecture.md — Unit Testing Architecture and System Design387- references/unit-testing-fundamentals.md — Unit Testing Fundamentals388- references/unit-testing-patterns.md — Unit Testing Patterns389- references/unit-testing-workflow-strategies.md — Unit Testing Workflow Strategies and Decision Frameworks390391## Handoff392After unit testing, hand off to:393- `quality-integration-testing` — for verifying component interactions with real dependencies394- `quality-property-based-testing` — for adding property-based invariants to complement examples395- `quality-regression-testing` — for regression suite execution and maintenance396- `quality-smoke-testing` — for BVT smoke test definition on tested components397## Implementation Patterns398399### Observer Pattern for Event Handling400`401interface EventObserver<T> {402 onEvent(event: T): Promise<void>;403}404405class EventBus<T> {406 private observers: Set<EventObserver<T>> = new Set();407 subscribe(observer: EventObserver<T>): void {408 this.observers.add(observer);409 }410 unsubscribe(observer: EventObserver<T>): void {411 this.observers.delete(observer);412 }413 async emit(event: T): Promise<void> {414 const results = Array.from(this.observers).map(o => o.onEvent(event));415 await Promise.allSettled(results);416 }417}418`419420### Configuration-Driven Approach421`422config:423 defaults:424 timeout: 30s425 retryCount: 3426 overrides:427 production:428 timeout: 60s429 retryCount: 5430 development:431 timeout: 300s432 retryCount: 1433`434435## Production Considerations436437### Deployment Checklist438- [ ] Configuration validated against schema before startup439- [ ] Health check endpoints registered and monitored440- [ ] Graceful shutdown with draining period (30s timeout)441- [ ] Resource limits configured (CPU, memory, file descriptors)442- [ ] Log level set appropriate for environment443- [ ] Metrics endpoint secured and exposed444- [ ] Rate limiting configured per-tier445- [ ] TLS certificates valid and auto-renewing446- [ ] Database migrations run as separate deployment step447- [ ] Feature flags ready for gradual rollout448449### Monitoring and Alerting450| Metric | Threshold | Severity | Action |451|--------|-----------|----------|--------|452| Error rate | > 1% over 5min | Critical | Page on-call |453| p99 latency | > 2s over 5min | Warning | Investigate |454| Throughput drop | > 50% over 1min | Critical | Check upstream |455| Queue depth | > 1000 over 1min | Warning | Scale consumers |456| Disk usage | > 85% | Warning | Clean or expand |457| Memory usage | > 90% heap | Critical | Restart or scale |458459## Anti-Patterns460461| Anti-Pattern | Symptom | Root Cause | Solution |462|-------------|---------|------------|----------|463| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |464| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |465| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |466| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |467468## Performance Optimization469470### Caching Strategy471Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge).472Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).473474### Resource Pooling475- Database connections: Pool of reusable connections (HikariCP, pgBouncer)476- HTTP connections: Keep-alive + connection pooling for external calls477- Thread pool: Bounded thread pools for async task execution478479### Profiling Methodology4801. Establish baseline with production traffic profile4812. Profile CPU with sampling profiler (pprof, perf, async-profiler)4823. Profile memory with heap dumps and allocation tracking4834. Profile I/O with strace/perf trace for syscall analysis4845. Profile latency with distributed tracing (OpenTelemetry)4856. Identify bottleneck, formulate hypothesis, implement fix4867. Re-profile to verify improvement, repeat487488## Security Considerations489490### Threat Modeling (STRIDE)491- Spoofing: Identity validation, authentication492- Tampering: Integrity checks, digital signatures493- Repudiation: Audit logs, non-repudiation494- Information disclosure: Encryption, access control495- Denial of service: Rate limiting, resource quotas496- Elevation of privilege: Principle of least privilege497498### Supply Chain Security499- Dependency scanning: Snyk, Dependabot, Trivy500- SBOM generation: CycloneDX or SPDX format501- Signed commits: GPG or SSH commit signing502- Artifact verification: Checksum validation, signature verification503504### Secrets Management505- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)506- Rotation policy: Rotate database credentials every 90 days507- Access audit: Log every secrets access, alert on anomalies508- Encryption at rest and in transit for all secrets509- Principle of least privilege: each service gets only its own secrets510511## Rules512- Default-deny security posture — allow only explicitly required access.513- All inputs validated, all outputs encoded, all errors handled.514- Defend in depth — multiple layers of security controls.515- Fail securely — errors default to safe behavior.516- Log security-relevant events for audit and investigation.517- Keep dependencies updated — automate vulnerability scanning.518- Design for observability from day one, not as an afterthought.519- Document all architectural decisions with rationale.520- Review code for security, performance, and correctness before merging.521## Architecture Decision Trees522523### Unit Test Approach524| Decision Point | Option A | Option B | Decision Criteria |525|---|---|---|---|526| Test framework | Jest (JS/TS ecosystem) | Vitest (faster, ESM-native) | Framework maturity, ESM compatibility |527| Assertion style | expect(x).toBe(y) (readable) | assert.equal(x, y) (minimal) | Team preference, existing codebase |528| Mocking style | Jest mocks (built-in) | ts-mockito/typed-mock | Type safety, TypeScript usage |529| Coverage tool | Built-in (Jest Istanbul) | c8 (modern, ESM-friendly) | Coverage needs, configuration complexity |530531### What to Unit Test532- Business logic → Pure functions, complex algorithms533- Data transformations → Mappers, serializers, validators534- Edge cases → Empty states, boundary values, error conditions535- NOT trivial → Simple getters/setters, framework wiring