Use Cases (Updated: 6/2/2026)

Claude Code TDD Guide: Test-Driven Development with Vitest and node:test

Practice TDD with Claude Code using working Vitest and node:test examples, CI, hooks, and prompt templates.

Claude Code TDD Guide: Test-Driven Development with Vitest and node:test

Claude Code can produce working code quickly, but speed alone does not make a change safe. The risky part usually appears later: a misunderstood edge case, a broken regression test, a discount bug, or a CI failure that was never reproduced locally.

Test-driven development, or TDD, is a practical way to keep that speed under control. The loop is simple: write a failing test first, make the smallest implementation pass, then refactor without changing behavior. This is the Red-Green-Refactor cycle. Red means the test fails for the right reason. Green means the behavior is implemented. Refactor means the code is cleaned up while the test suite stays green.

Claude Code fits this workflow well because it can enumerate cases, read failing output, implement the smallest fix, and update CI. The key is to avoid asking it to “just build the feature.” Ask it to prove the failure first. This article uses copy-pasteable Vitest and node:test examples, current Claude Code hook syntax, CI configuration, and prompt templates you can reuse.

The references checked for this update were the official Claude Code hooks reference, Claude Code memory, Claude Code settings, Vitest Getting Started, Vitest CLI, and the Node.js test runner. The hook example below reads JSON from stdin, matching the current Claude Code docs instead of older environment-variable-only snippets.

Where Claude Code Helps in TDD

Claude Code is useful for generating test cases, writing the first implementation, interpreting failures, adding CI, and summarizing residual risk. Humans should still decide the business rules, security boundaries, public API contract, and release decision.

StepClaude Code taskHuman review
RedWrite failing tests from the specDid it invent requirements?
GreenImplement the smallest passing changeDid it add unnecessary abstractions?
RefactorRemove duplication and improve namesDid behavior remain unchanged?
CIRun tests on every PRIs the Node version realistic?
OperationsUse hooks and CLAUDE.mdIs automation fast enough?
flowchart LR
  A["Slice the spec"] --> B["Red: failing test"]
  B --> C["Green: smallest implementation"]
  C --> D["Refactor: clean up"]
  D --> E["CI and hooks rerun tests"]
  E --> B

Example 1: Red-Green-Refactor with Vitest

Pricing logic is a good first TDD target because small mistakes affect revenue directly. Install Vitest first.

npm install -D vitest
{
  "type": "module",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "devDependencies": {
    "vitest": "^3.0.0"
  }
}

Create src/cart.test.ts before the implementation exists.

import { describe, expect, it } from "vitest";
import { priceCart, ValidationError } from "./cart";

describe("priceCart", () => {
  it("calculates subtotal and total without a coupon", () => {
    const result = priceCart({
      items: [
        { sku: "book", unitPriceCents: 1200, quantity: 2 },
        { sku: "video", unitPriceCents: 3000, quantity: 1 },
      ],
    });

    expect(result).toEqual({
      subtotalCents: 5400,
      discountCents: 0,
      totalCents: 5400,
    });
  });

  it("applies a valid percent coupon", () => {
    const result = priceCart(
      {
        items: [{ sku: "course", unitPriceCents: 10000, quantity: 1 }],
        coupon: {
          code: "SPRING20",
          percentOff: 20,
          expiresAt: "2026-06-30T00:00:00.000Z",
        },
      },
      { now: new Date("2026-06-02T00:00:00.000Z") },
    );

    expect(result.totalCents).toBe(8000);
    expect(result.discountCents).toBe(2000);
  });

  it("rejects expired coupons", () => {
    expect(() =>
      priceCart(
        {
          items: [{ sku: "course", unitPriceCents: 10000, quantity: 1 }],
          coupon: {
            code: "OLD20",
            percentOff: 20,
            expiresAt: "2026-05-01T00:00:00.000Z",
          },
        },
        { now: new Date("2026-06-02T00:00:00.000Z") },
      ),
    ).toThrow(ValidationError);
  });

  it("rejects zero or negative quantity", () => {
    expect(() =>
      priceCart({
        items: [{ sku: "book", unitPriceCents: 1200, quantity: 0 }],
      }),
    ).toThrow("quantity must be positive");
  });
});

Ask Claude Code to prove Red before it writes src/cart.ts.

We are in the Red step. src/cart.test.ts exists, but src/cart.ts does not.

Please:
1. Run npm test and confirm the failure.
2. Implement only the smallest src/cart.ts needed to pass.
3. Do not add UI, DB calls, external APIs, or future features.
4. Refactor only after the targeted tests are green.

Then use this implementation.

export class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

type CartItem = {
  sku: string;
  unitPriceCents: number;
  quantity: number;
};

type Coupon = {
  code: string;
  percentOff: number;
  expiresAt: string;
};

type CartInput = {
  items: CartItem[];
  coupon?: Coupon;
};

type PriceOptions = {
  now?: Date;
};

export function priceCart(input: CartInput, options: PriceOptions = {}) {
  if (input.items.length === 0) {
    throw new ValidationError("cart must contain at least one item");
  }

  const subtotalCents = input.items.reduce((sum, item) => {
    if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
      throw new ValidationError("quantity must be positive");
    }
    if (!Number.isInteger(item.unitPriceCents) || item.unitPriceCents < 0) {
      throw new ValidationError("unitPriceCents must be a non-negative integer");
    }
    return sum + item.unitPriceCents * item.quantity;
  }, 0);

  const discountCents = calculateDiscount(subtotalCents, input.coupon, options.now ?? new Date());

  return {
    subtotalCents,
    discountCents,
    totalCents: subtotalCents - discountCents,
  };
}

function calculateDiscount(subtotalCents: number, coupon: Coupon | undefined, now: Date) {
  if (!coupon) return 0;

  if (coupon.percentOff <= 0 || coupon.percentOff > 100) {
    throw new ValidationError("percentOff must be between 1 and 100");
  }

  if (new Date(coupon.expiresAt).getTime() < now.getTime()) {
    throw new ValidationError("coupon expired");
  }

  return Math.round(subtotalCents * (coupon.percentOff / 100));
}

Example 2: Boundary Values with node:test

For small Node utilities, the built-in test runner keeps dependencies low. Save this as limit.test.mjs and run it with node --test limit.test.mjs.

import test from "node:test";
import assert from "node:assert/strict";

export function parseLimit(value, fallback = 20) {
  if (value === undefined || value === "") return fallback;

  const parsed = Number(value);
  if (!Number.isInteger(parsed)) {
    throw new TypeError("limit must be an integer");
  }
  if (parsed < 1 || parsed > 100) {
    throw new RangeError("limit must be between 1 and 100");
  }

  return parsed;
}

test("parseLimit uses fallback when the value is empty", () => {
  assert.equal(parseLimit(undefined), 20);
  assert.equal(parseLimit("", 50), 50);
});

test("parseLimit accepts values from 1 to 100", () => {
  assert.equal(parseLimit("1"), 1);
  assert.equal(parseLimit("100"), 100);
});

test("parseLimit rejects decimals and out-of-range values", () => {
  assert.throws(() => parseLimit("1.5"), /integer/);
  assert.throws(() => parseLimit("0"), /between 1 and 100/);
  assert.throws(() => parseLimit("101"), /between 1 and 100/);
});
node --test limit.test.mjs

Example 3: Regression Tests for an API Bug

When a production bug appears, turn it into a failing test before changing the implementation.

Add a regression test using TDD.

Background:
- POST /checkout incorrectly accepts expired coupons.
- Valid coupons and no-coupon checkout must continue to work.

Red:
- Add a test that expects 400 for an expired coupon.
- Confirm the current implementation fails that test.

Green:
- Make the smallest API change needed to pass.

Refactor:
- Extract only duplicated date comparison logic.

Return:
- Test name, failing output, changed files, commands run, remaining risk.

For broader testing patterns, see the API testing guide and the testing strategy guide.

CI and Hooks

CI keeps the loop honest.

name: test
on:
  pull_request:
  push:
    branches: [main]

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm test

Claude Code hooks can run related tests after edits. Current command hooks receive JSON on stdin, so the script reads tool_input.file_path.

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/run-related-vitest.mjs",
            "timeout": 120
          }
        ]
      }
    ]
  }
}
import { spawnSync } from "node:child_process";
import path from "node:path";

let raw = "";
for await (const chunk of process.stdin) {
  raw += chunk;
}

const event = raw ? JSON.parse(raw) : {};
const filePath = event.tool_input?.file_path;

if (typeof filePath !== "string" || !/\.[cm]?[jt]sx?$/.test(filePath)) {
  process.exit(0);
}

const target = path.isAbsolute(filePath)
  ? path.relative(process.cwd(), filePath)
  : filePath;

const result = spawnSync("npx", ["vitest", "related", target, "--run"], {
  stdio: "inherit",
  shell: process.platform === "win32",
});

process.exit(result.status ?? 1);

Keep this hook small. Run related tests on edit, then leave slow E2E suites to CI. For deeper setup patterns, read the hooks guide and the CLAUDE.md best practices.

Prompt Templates

New feature TDD:
Goal:
  Add [feature].
Spec:
  - [happy path]
  - [boundary values]
  - [failure behavior]
Process:
  1. Write tests first.
  2. Run npm test and show Red.
  3. Implement the smallest Green change.
  4. Refactor without changing behavior.
Return:
  Failure output, commands, changed files, remaining risk.
Bug fix TDD:
Reproduction:
  [input, user action, or log]
Expected:
  [correct behavior]
Current:
  [actual behavior]
Request:
  Add a failing regression test first.
  Then make the smallest fix.
  Do not weaken or delete existing tests without explaining why.
Refactor with safety:
Target:
  [file/function]
Constraint:
  Public behavior must not change.
Steps:
  1. Add characterization tests for current behavior.
  2. Confirm they are green.
  3. Refactor internals only.
  4. Re-run the same tests.

Common Pitfalls

The first pitfall is skipping Red. If the test passes before the fix, it is not protecting anything. The second is testing implementation details instead of behavior. For pricing, totals and errors matter more than private helper names. The third is using real time inside tests. Inject now so expired coupons do not become flaky next month. The fourth is trusting mocks too much; payment, email, and CRM flows need contract or staging checks. The fifth is allowing Claude Code to delete tests just to get Green.

CTA

Start with one pricing rule, CLI parser, or API regression. Keep the proof small and repeatable. Solo builders can use the free Claude Code cheatsheet and the templates above. If you want reusable prompts, hooks, and review checklists, browse ClaudeCodeLab products. Teams that need TDD, CI, permissions, and review workflows adapted to a real repository can use Claude Code training and consultation.

What Happened When We Tried This

In Masa’s workflow, asking Claude Code for the failing test first reduced review time more than asking for implementation first. Coupon expiry, zero quantity, and unauthenticated API paths surfaced earlier. Running every test from a hook was too slow, so the practical setup became related Vitest tests after edits and full E2E in CI. TDD worked best as a lightweight guardrail for Claude Code’s speed, not as a ceremony.

#Claude Code #TDD #test-driven development #testing #quality assurance
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.