| name | tdd-workflow |
| description | Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit and integration tests. |
| origin | ECC |
Test-Driven Development Workflow
This skill ensures all code development follows TDD principles with comprehensive test coverage.
When to Activate
- Writing new CLI commands or utilities
- Fixing bugs (write test that reproduces bug first)
- Refactoring existing code
- Adding new option parsing logic
- Extending display helpers
Core Principles
1. Tests BEFORE Code
ALWAYS write tests first, then implement code to make tests pass.
2. Coverage Requirements
- Minimum 80% coverage (unit + integration)
- All edge cases covered
- Error scenarios tested
- Boundary conditions verified
3. Test Types
Unit Tests
- Individual utility functions in
src/utils/
- Option parsing logic (
parseXxxOptions)
- Type guards and brand type constructors
- Pure functions
Integration Tests
- Full CLI command behavior end-to-end
- Spawn the built CLI binary, assert stdout/stderr/exit code
- Use fixtures in
test/fixtures/
TDD Workflow Steps
Step 1: Define the Interface
export function parsePort(raw: string | undefined): number {
throw new Error("Not implemented");
}
Step 2: Write Failing Test (RED)
import { describe, it, expect } from "vitest";
import { parsePort } from "~/utils/someUtil.js";
describe("parsePort", () => {
it("returns default port when undefined", () => {
expect(parsePort(undefined)).toBe(3000);
});
it("parses valid port string", () => {
expect(parsePort("8080")).toBe(8080);
});
it("throws on invalid port", () => {
expect(() => parsePort("invalid")).toThrow("Invalid port");
});
it("throws on out-of-range port", () => {
expect(() => parsePort("99999")).toThrow("Invalid port");
});
});
Step 3: Run Tests — They Should FAIL
bun run test
Step 4: Implement Minimal Code (GREEN)
export function parsePort(raw: string | undefined): number {
if (raw === undefined) return 3000;
const port = Number(raw);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port: ${raw}. Must be 1-65535.`);
}
return port;
}
Step 5: Run Tests — They Should PASS
bun run test
Step 6: Refactor (IMPROVE)
Improve code quality while keeping tests green.
Step 7: Verify Coverage
bun run test:coverage
Testing Patterns
Unit Test Pattern (Vitest)
import { describe, it, expect, vi, beforeEach } from "vitest";
describe("isCacheValid", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns false when file does not exist", () => {
vi.mock("fs", () => ({
existsSync: vi.fn().mockReturnValue(false),
statSync: vi.fn(),
}));
expect(isCacheValid("/tmp/nonexistent.md")).toBe(false);
});
it("returns false when cache is expired", () => {
vi.mock("fs", () => ({
existsSync: vi.fn().mockReturnValue(true),
statSync: vi.fn().mockReturnValue({ mtimeMs: Date.now() - 25 * 60 * 60 * 1000 }),
}));
expect(isCacheValid("/tmp/old.md")).toBe(false);
});
it("returns true when cache is fresh", () => {
vi.mock("fs", () => ({
existsSync: vi.fn().mockReturnValue(true),
statSync: vi.fn().mockReturnValue({ mtimeMs: Date.now() - 1000 }),
}));
expect(isCacheValid("/tmp/fresh.md")).toBe(true);
});
});
Result Type Test Pattern
import { describe, it, expect } from "vitest";
describe("fetchDoc", () => {
it("returns Ok on success", async () => {
vi.mock("~/utils/fetcher.js", () => ({
docsFetcher: vi.fn().mockResolvedValue("# Hello World"),
}));
const result = await fetchDoc("essential/route" as DocsRepoRelativePath);
expect(result.isOk()).toBe(true);
result.match({
ok: (content) => expect(content).toContain("Hello World"),
err: () => {
throw new Error("Should not be error");
},
});
});
it("returns Err on 404", async () => {
vi.mock("~/utils/fetcher.js", () => ({
docsFetcher: vi
.fn()
.mockRejectedValue(Object.assign(new Error("Not Found"), { status: 404 })),
}));
const result = await fetchDoc("missing/path" as DocsRepoRelativePath);
expect(result.isErr()).toBe(true);
result.match({
ok: () => {
throw new Error("Should not be ok");
},
err: (e) => expect(e.message).toContain("not found"),
});
});
});
CLI Integration Test Pattern
import { describe, it, expect } from "vitest";
import { spawnSync } from "child_process";
import { resolve } from "path";
const CLI = resolve("dist/cli.js");
describe("elysia search", () => {
it("outputs JSON array for valid query", () => {
const { stdout, status } = spawnSync("node", [CLI, "search", "route"], {
encoding: "utf-8",
});
expect(status).toBe(0);
const results = JSON.parse(stdout);
expect(Array.isArray(results)).toBe(true);
});
it("returns empty array when no results", () => {
const { stdout, status } = spawnSync("node", [CLI, "search", "xyznotfound12345"], {
encoding: "utf-8",
});
expect(status).toBe(0);
expect(JSON.parse(stdout)).toEqual([]);
});
});
Test File Organization
test/
├── commands/
│ ├── display.test.ts # src/utils/display.ts tests
│ ├── loader.test.ts # src/utils/loader.ts tests
│ ├── request.test.ts # request command logic tests
│ ├── routes.test.ts # route extraction tests
│ └── runtime.test.ts # Bun/Node detection tests
├── integration/
│ └── cli.integration.test.ts # full CLI end-to-end tests
└── fixtures/
├── basic-app.ts # Elysia app used by tests
└── serve-http-entry.ts # serve command fixture
Mocking in Vitest (NOT Jest)
import { vi } from "vitest";
const mockFn = vi.fn();
const mockAsync = vi.fn().mockResolvedValue("value");
vi.mock("~/utils/fetcher.js", () => ({
docsFetcher: vi.fn(),
}));
vi.spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit called");
});
beforeEach(() => vi.clearAllMocks());
afterEach(() => vi.restoreAllMocks());
jest.fn();
jest.mock();
Coverage Thresholds (vitest.config.ts)
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
});
Common Testing Mistakes to Avoid
❌ WRONG: Using Jest API
jest.fn();
jest.mock("./foo");
✅ CORRECT: Using Vitest API
vi.fn();
vi.mock("./foo");
❌ WRONG: No Test Isolation
let cache = {};
test("sets cache", () => {
cache["key"] = "value";
});
test("reads cache", () => {
expect(cache["key"]).toBe("value");
});
✅ CORRECT: Independent Tests
test("sets cache", () => {
const cache = {};
cache["key"] = "value";
expect(cache["key"]).toBe("value");
});
Continuous Testing
bun run test:watch
bun run test
bun run test:build
Best Practices
- Write Tests First - Always TDD
- One Behavior Per Test - Focus on single behavior
- Descriptive Test Names - Explain what's tested
- Arrange-Act-Assert - Clear test structure
- Use
vi.* - This is Vitest, NOT Jest
- Test Edge Cases - Null, undefined, empty, invalid, out-of-range
- Test Error Paths - Not just happy paths
- Keep Tests Fast - Unit tests < 50ms each
- No Shared State - Each test sets up its own data
Remember: Tests are not optional. They are the safety net that enables confident refactoring and reliable releases.