Claude Code Testing Strategy: Vitest, Testing Library, Playwright, and CI
Build a practical Claude Code testing strategy with unit, integration, E2E, and CI checks.
A testing strategy is not a promise to write more tests. It is a decision about what deserves fast unit coverage, what needs integration confidence, and which business-critical flows should be protected by E2E tests.
When you ask Claude Code to “add tests” without scope, it may generate shallow assertions, brittle CSS selectors, or tests that only mirror the current implementation. The better request gives Claude Code a test layer, target behavior, command to run, and clear boundaries.
The guidance below was checked against the official Claude Code common workflows, Vitest coverage guide, Testing Library query priority, and Playwright docs for locators, assertions, and CI.
Start With The Pyramid
flowchart TB
E2E["E2E: signup, checkout, paid CTA"]
INT["Integration: API, DB, forms, components"]
UNIT["Unit: calculation, validation, permissions"]
E2E --> INT --> UNIT
| Layer | Target share | What it protects | Tools |
|---|---|---|---|
| Unit | 60-70% | pure logic, validation, permissions | Vitest |
| Integration | 20-30% | components, API, database boundaries | Vitest + Testing Library |
| E2E | 5-10% | signup, checkout, revenue paths | Playwright |
| CI gate | every PR | lint, types, tests, reports | GitHub Actions |
Use the table before prompting Claude Code. Pricing logic belongs in unit tests. A checkout button belongs in integration tests. A reader moving from an article CTA to the products page is an E2E flow.
Minimal Setup
npm i -D vitest @vitest/coverage-v8 jsdom \
@testing-library/react @testing-library/jest-dom @testing-library/user-event \
@playwright/test
npx playwright install --with-deps
{
"scripts": {
"test": "vitest --run",
"test:watch": "vitest",
"test:coverage": "vitest --run --coverage",
"test:e2e": "playwright test"
}
}
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
coverage: {
provider: "v8",
reporter: ["text", "html"],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
},
},
});
// test/setup.ts
import "@testing-library/jest-dom/vitest";
Vitest currently documents both v8 and istanbul coverage providers. For Node and Chromium-based projects, v8 is the practical default.
Example 1: Unit Test Pricing Logic
// src/lib/pricing.ts
export type PriceInput = {
unitPrice: number;
quantity: number;
discountRate?: number;
taxRate?: number;
};
export function calculateTotal({
unitPrice,
quantity,
discountRate = 0,
taxRate = 0.1,
}: PriceInput): number {
if (!Number.isInteger(quantity) || quantity < 0) {
throw new Error("quantity must be a non-negative integer");
}
if (unitPrice < 0) throw new Error("unitPrice must be non-negative");
if (discountRate < 0 || discountRate > 1) {
throw new Error("discountRate must be between 0 and 1");
}
const discounted = unitPrice * quantity * (1 - discountRate);
return Math.round(discounted * (1 + taxRate));
}
// src/lib/pricing.test.ts
import { describe, expect, it } from "vitest";
import { calculateTotal } from "./pricing";
describe("calculateTotal", () => {
it("calculates a tax-included total", () => {
expect(calculateTotal({ unitPrice: 1000, quantity: 2 })).toBe(2200);
});
it("applies discount before tax", () => {
expect(
calculateTotal({ unitPrice: 1000, quantity: 2, discountRate: 0.2 })
).toBe(1760);
});
it("allows zero quantity", () => {
expect(calculateTotal({ unitPrice: 1000, quantity: 0 })).toBe(0);
});
it("rejects invalid inputs", () => {
expect(() => calculateTotal({ unitPrice: 1000, quantity: -1 })).toThrow(
"quantity must be a non-negative integer"
);
});
});
The trap is chasing 80% coverage with only happy paths. Ask Claude Code for success cases, boundary values, and failure cases separately.
Example 2: Test A Revenue CTA Component
Testing Library recommends queries that resemble how users find elements. Prefer role and label queries before test IDs.
// src/components/CheckoutButton.tsx
import { useState } from "react";
type Props = {
stock: number;
onCheckout: () => Promise<void>;
};
export function CheckoutButton({ stock, onCheckout }: Props) {
const [submitting, setSubmitting] = useState(false);
const soldOut = stock <= 0;
async function handleClick() {
setSubmitting(true);
try {
await onCheckout();
} finally {
setSubmitting(false);
}
}
return (
<button
type="button"
disabled={soldOut || submitting}
aria-busy={submitting}
onClick={handleClick}
>
{soldOut ? "Sold out" : submitting ? "Processing..." : "Buy now"}
</button>
);
}
// src/components/CheckoutButton.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { CheckoutButton } from "./CheckoutButton";
describe("CheckoutButton", () => {
it("calls checkout when in stock", async () => {
const user = userEvent.setup();
const onCheckout = vi.fn().mockResolvedValue(undefined);
render(<CheckoutButton stock={3} onCheckout={onCheckout} />);
await user.click(screen.getByRole("button", { name: "Buy now" }));
expect(onCheckout).toHaveBeenCalledTimes(1);
});
it("prevents checkout when sold out", async () => {
const user = userEvent.setup();
const onCheckout = vi.fn().mockResolvedValue(undefined);
render(<CheckoutButton stock={0} onCheckout={onCheckout} />);
const button = screen.getByRole("button", { name: "Sold out" });
expect(button).toBeDisabled();
await user.click(button);
expect(onCheckout).not.toHaveBeenCalled();
});
});
Avoid querySelector(".primary") and snapshot-only tests for CTAs. They break on design changes while missing the behavior that affects revenue.
Example 3: Keep E2E Small
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
const baseURL =
process.env.PLAYWRIGHT_TEST_BASE_URL ?? "http://127.0.0.1:5173";
export default defineConfig({
testDir: "./e2e",
retries: process.env.CI ? 2 : 0,
use: { baseURL, trace: "on-first-retry" },
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: process.env.PLAYWRIGHT_TEST_BASE_URL
? undefined
: {
command: "npm run dev -- --host 127.0.0.1",
url: baseURL,
reuseExistingServer: !process.env.CI,
},
});
// e2e/article-cta.spec.ts
import { expect, test } from "@playwright/test";
test("reader can move from article CTA to products", async ({ page }) => {
await page.goto("/en/blog/claude-code-testing-strategies");
await page.getByRole("link", { name: /products|templates/i }).click();
await expect(page).toHaveURL(/\/products\/?$/);
await expect(page.getByRole("heading", { name: /products/i })).toBeVisible();
});
Playwright’s web-first assertions retry automatically. Prefer await expect(locator).toBeVisible() over fixed sleeps. Use role, label, and stable test IDs rather than deep CSS paths or nth().
Example 4: Add A CI Gate
# .github/workflows/test.yml
name: Test
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:coverage
- run: npx playwright install --with-deps
- run: npm run test:e2e
env:
CI: "true"
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
If you use Claude Code GitHub Actions, the current official docs show anthropics/claude-code-action@v1 for the GA action. Avoid copying older @beta examples without checking the docs.
Prompt Templates For Claude Code
Add Vitest unit tests for src/lib/pricing.ts.
Cover success cases, boundary values, and invalid inputs.
Do not change implementation unless you first list the suspected bug.
Run npm run test -- pricing and summarize the result.
Add Testing Library tests for src/components/CheckoutButton.tsx.
Use getByRole and userEvent.setup().
Do not use CSS selectors, snapshots as the main assertion, or implementation details.
Cover in-stock, sold-out, and submitting states.
Add one Playwright E2E test for the article-to-products CTA flow.
Avoid waitForTimeout, deep CSS selectors, and nth().
Use web-first assertions and keep the test focused on the revenue path.
Inspect the latest CI failure.
Classify it as lint, typecheck, unit, e2e, or environment.
Fix the root cause with the smallest diff.
Do not skip tests or only increase timeouts.
Pitfalls
Do not mock everything. Mock payment providers and email delivery, but test pricing, permissions, and persistence where they matter.
Do not let E2E tests freeze every visual detail. They should protect reader actions, signup, checkout, and support contact paths.
Do not treat coverage as proof. A project can show 80% coverage and still miss the branch that deletes data or charges money.
Do not prompt Claude Code with only the happy path. Include the anti-requirements: no brittle selectors, no skipped tests, no snapshot-only coverage, and no hidden implementation coupling.
Before merging, ask for a short review note that lists what was tested, what was intentionally not tested, and which command produced the evidence. That note is boring but useful. It stops a future reviewer from guessing whether a Playwright failure was ignored, whether a mocked payment path was deliberate, or whether the coverage report came from a local watch run instead of CI.
CTA
Testing protects revenue paths as much as code quality. Start with the free Claude Code cheatsheet, turn repeated review language into reusable products and templates, and use Claude Code training when the team needs CI, review rules, and test ownership set up together. For adjacent topics, see TDD with Claude Code, CI/CD setup, and debugging techniques.
What I Actually Tried
Masa tested this pattern on ClaudeCodeLab article CTAs and product-page flows. The strongest result came from keeping pricing logic in unit tests, CTA behavior in Testing Library, and only one Playwright test for the article-to-products path. Asking Claude Code to list failure modes first reduced skipped tests and brittle selectors. The remaining risk is external payment and ad scripts, which still need CI evidence plus a short manual release check.
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.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Receipt Pattern: Record Scope, Proof, and Rollback
A permission receipt pattern for Claude Code: allowed actions, approval boundaries, proof commands, rollback, and revenue CTA checks.
Safe Agent Harness Design for Claude Code and Codex: Permissions, Checks, and Rollback
Build a practical agent harness for Claude Code and Codex with policy, planning, verification, and recovery layers.
Claude Code Subagents: A Practical Guide to Safe Agent Delegation
Claude Code subagent guide for safe parallel article and code work: delegation rules, prompts, pitfalls, and checks.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.