tdd

SKILL.md

Test-Driven Development (TDD)

Philosophy

TDD is a development methodology where you write tests BEFORE implementation. This ensures:

  • Clear requirements - Tests define expected behavior upfront
  • Better design - Forces you to think about interfaces first
  • Confidence - Every feature has test coverage from day one
  • Refactoring safety - Tests catch regressions immediately

When to Use TDD

Situation Use TDD? Reason
New utility function YES Pure functions are perfect for TDD
New Effect service YES Define interface via tests first
Complex business logic YES Tests clarify requirements
Bug fix YES Write failing test that reproduces bug first
UI component styling No Visual changes don't benefit from TDD
Exploratory prototyping No Requirements unclear, iterate first
TRPC endpoint (simple CRUD) Optional Ask user preference

Red-Green-Refactor Cycle

The TDD workflow follows three phases:

1. RED - Write Failing Test First

import { describe, expect, it } from "vitest";
import { calculateDiscount } from "../calculate-discount";

describe("calculateDiscount", () => {
  it("applies 10% discount for orders over 100", () => {
    // This test will FAIL - function doesn't exist yet
    expect(calculateDiscount(150)).toBe(135);
  });
});

Run test to see it fail:

bun run vitest run packages/common/src/__tests__/calculate-discount.test.ts

2. GREEN - Minimal Implementation

Write the minimum code to make the test pass:

// packages/common/src/calculate-discount.ts
export function calculateDiscount(amount: number): number {
  if (amount > 100) {
    return amount * 0.9;
  }
  return amount;
}

Run test to see it pass:

bun run vitest run packages/common/src/__tests__/calculate-discount.test.ts

3. REFACTOR - Improve Without Breaking

Improve code quality while keeping tests green:

const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.1;

export function calculateDiscount(amount: number): number {
  if (amount <= DISCOUNT_THRESHOLD) {
    return amount;
  }
  return amount * (1 - DISCOUNT_RATE);
}

Run tests again to verify refactoring didn't break anything.

See references/red-green-refactor.md for detailed workflow examples.


Test Hierarchy (Prefer Simpler)

  1. Unit tests (preferred) - Pure functions, Effect services with mock layers
  2. TRPC Integration (ask first) - Full TRPC stack with PGlite
  3. E2E (ask + justify) - Browser automation, slowest
Situation Test Type Action
Pure function, parser, util Unit Write immediately
Effect service with dependencies Unit with mock layers Write immediately
TRPC procedure (DB logic) TRPC Integration Ask user first
User-facing flow, UI behavior E2E Ask + warn about maintenance

Effect TDD Patterns

Test-First Service Design

  1. Define interface via test - What should the service do?
  2. Create mock layer - Isolate dependencies
  3. Implement service - Make tests pass
  4. Refactor - Improve with confidence
import { describe, expect, it } from "@effect/vitest";
import { Effect, Layer } from "effect";

describe("PricingService", () => {
  // 1. Define what the service should do via tests
  it.effect("calculates base price without discount", () =>
    Effect.gen(function* () {
      const service = yield* PricingService;
      const result = yield* service.calculatePrice({
        itemId: "item-1",
        quantity: 2,
      });
      expect(result.total).toBe(200);
    }).pipe(Effect.provide(testLayer)),
  );

  it.effect("applies bulk discount for quantity > 10", () =>
    Effect.gen(function* () {
      const service = yield* PricingService;
      const result = yield* service.calculatePrice({
        itemId: "item-1",
        quantity: 15,
      });
      expect(result.total).toBe(1350); // 15% discount
    }).pipe(Effect.provide(testLayer)),
  );
});

Mock Layer Factory Pattern

// Create parameterized mock layers for different test scenarios
const createMockInventoryLayer = (inventory: Map<string, number>) =>
  Layer.succeed(InventoryService, {
    getStock: (itemId) => Effect.succeed(inventory.get(itemId) ?? 0),
    reserveStock: (itemId, qty) => Effect.succeed(void 0),
  });

// Use in tests
const testLayer = PricingService.layer.pipe(
  Layer.provide(createMockInventoryLayer(new Map([["item-1", 100]]))),
);

See references/effect-tdd-patterns.md for comprehensive Effect testing patterns.


Test File Locations

Code Location Test Location
packages/X/src/file.ts packages/X/src/__tests__/file.test.ts
apps/web-app/src/infrastructure/trpc/routers/X.ts apps/web-app/src/__tests__/X.test.ts
apps/web-app/src/routes/** apps/web-app/e2e/feature.e2e.ts

Commands

Unit & Integration Tests

# Run all tests (unit + TRPC integration)
bun run test

# Watch mode - re-run on file changes
bun run test:watch

# Run with coverage report
bun run test:coverage

# Run specific test file (FROM PROJECT ROOT, full path required)
bun run vitest run packages/common/src/__tests__/pagination.test.ts
bun run vitest run apps/web-app/src/__tests__/formatters.test.ts

# Run tests matching pattern
bun run vitest run -t "calculateDiscount"

E2E Tests

# Install Playwright browsers (first time only)
bun run test:e2e:install

# Run all E2E tests
bun run test:e2e

# Run E2E with interactive UI
bun run test:e2e:ui

WRONG Syntax (DO NOT USE)

# These DO NOT work:
bun run test packages/common/src/__tests__/file.test.ts  # script doesn't accept path
cd packages/common && bun run vitest run src/__tests__/file.test.ts  # wrong cwd

TDD Anti-Patterns

1. Writing Implementation First

// ❌ BAD - Implementation before test
export function formatPrice(amount: number): string {
  return `$${amount.toFixed(2)}`;
}

// Then writing test after - defeats TDD purpose

2. Skipping the RED Phase

// ❌ BAD - Test passes immediately (you didn't verify it can fail)
it("returns true", () => {
  expect(true).toBe(true); // This always passes!
});

3. Too Many Tests at Once

// ❌ BAD - Writing all tests before any implementation
describe("UserService", () => {
  it("creates user", () => {
    /* ... */
  });
  it("updates user", () => {
    /* ... */
  });
  it("deletes user", () => {
    /* ... */
  });
  it("lists users", () => {
    /* ... */
  });
  it("validates email", () => {
    /* ... */
  });
  // 10 more tests...
});
// Now you have 15 failing tests - overwhelming!

Correct approach: One test at a time. RED → GREEN → REFACTOR → next test.

4. Skipping Refactor Phase

// ❌ BAD - Test passes, move on without cleanup
export function calc(a: number, b: number, c: string): number {
  if (c === "add") return a + b;
  if (c === "sub") return a - b;
  if (c === "mul") return a * b;
  return 0;
}

// ✅ GOOD - Refactor to cleaner design
type Operation = "add" | "subtract" | "multiply";

const operations: Record<Operation, (a: number, b: number) => number> = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
};

export function calculate(a: number, b: number, op: Operation): number {
  return operations[op](a, b);
}

Resources

references/

  • red-green-refactor.md - Detailed TDD cycle workflow with examples
  • effect-tdd-patterns.md - Effect service testing, mock layers, error cases
  • test-first-examples.md - Step-by-step TDD examples for this codebase

Related Skills

  • testing-patterns - Test syntax, TRPC integration tests, E2E patterns
  • effect-ts - Effect service design, layers, error handling
Weekly Installs
42
GitHub Stars
3
First Seen
Feb 28, 2026
Installed on
opencode42
claude-code40
codex23
gemini-cli22
github-copilot22
amp22