/ 3 min read

Fast Tests Beat Complete Tests


This is Part 5 of a series on plan-based development with Claude Code. Today we explore our testing philosophy: fast feedback over comprehensive coverage.


The Testing Philosophy


Our testing strategy prioritizes fast feedback over comprehensive coverage. This might sound backwards, but here’s the reasoning:


  • A test suite that takes 30 seconds to run gets executed constantly
  • A test suite that takes 10 minutes gets executed only in CI
  • Fast, frequent testing catches issues earlier in the development cycle

Test Infrastructure Setup


We use Vitest with shared configurations that optimize for speed:


tools/vitest/src/server.ts
export function createServerConfig(options: {
name: string;
testInclude?: string[];
}) {
return defineConfig({
test: {
name: options.name,
include: options.testInclude ?? ["src/**/*.test.ts"],
passWithNoTests: true, // Don't fail empty packages
testTimeout: 10000,
hookTimeout: 10000,
},
});
}

The passWithNoTests: true is crucial. It allows packages to exist without tests initially, enabling us to add tests incrementally rather than as a gate.


The Testing Pyramid in Practice


Our test distribution:


/\
/ \ E2E Tests (Playwright)
/----\ 5% - Critical user journeys
/ \
/--------\ Integration Tests
/ \ 15% - API endpoints, DB operations
/------------\
/ \ Unit Tests
/----------------\ 80% - Functions, utilities, components

Unit Tests: The Foundation


Fast, isolated, focused on pure logic:


packages/results-ai/src/__tests__/alienation.test.ts
describe("calculateAlienation", () => {
it("identifies clear risk when alienation exceeds threshold", () => {
const result = calculateAlienation({
controlResponses: mockControlData,
testResponses: mockTestData,
segments: [{ id: "gen-z", name: "Gen Z" }],
threshold: 4,
});
expect(result[0].verdict).toBe("clear_risk");
expect(result[0].alienatedProportion).toBeGreaterThan(0.15);
});
});

Integration Tests: The Safety Net


Test real interactions between systems:


packages/auth/src/__tests__/auth.integration.test.ts
describe("Authentication Flow", () => {
it("creates session on successful login", async () => {
const response = await authClient.signIn.email({
password: "password123",
});
expect(response.data?.session).toBeDefined();
expect(response.data?.user.email).toBe("[email protected]");
});
});

E2E Tests: The User Journey


Full browser tests for critical paths:


apps/web-pw/tests/auth/login.spec.ts
test("user can log in and view dashboard", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', TEST_CLIENT_EMAIL);
await page.fill('[name="password"]', TEST_CLIENT_PASSWORD);
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard");
await expect(page.locator("h1")).toContainText("Welcome");
});

Coverage as a Guide, Not a Goal


We track coverage but don’t chase arbitrary percentages. Coverage tells us where we lack tests, not whether we have enough. 100% coverage doesn’t mean 100% correct.


Practical Testing Patterns


Pattern 1: Test the Public API


// Don't test internal implementation
// DO test exported functions
describe("@bts/results-ai", () => {
it("exports expected tool names", async () => {
const { RESULTS_TOOL_NAMES } = await import("../index");
expect(RESULTS_TOOL_NAMES).toBeDefined();
expect(RESULTS_TOOL_NAMES.analyze).toBe("analyze_results");
});
});

Pattern 2: Handle Async Timeouts


it("handles slow operations", async () => {
// Dynamic imports can be slow in parallel test execution
const { slowFunction } = await import("./slow-module");
expect(slowFunction()).toBeDefined();
}, { timeout: 15000 }); // Explicit timeout

Pattern 3: Case-Insensitive Assertions


it("renders email correctly", async () => {
const text = await render(<Email {...props} />, { plainText: true });
// Email renderers may transform case
expect(text.toLowerCase()).toContain(props.subject.toLowerCase());
});

The CI Integration


Our GitHub Actions workflow runs tests in parallel:


- name: Run tests with coverage
run: bun run test:coverage
- name: Merge coverage reports
run: bun run coverage:merge
- name: Add coverage to job summary
run: cat coverage/coverage-report.md >> $GITHUB_STEP_SUMMARY

Coverage reports appear directly in PR comments, making review easier.


Next up in Part 6: Claude Code as your AI pair programmer - custom commands and skills that encode your team’s workflows.