Claude Code Testing Strategy: Unit vs Integration vs E2E — What to Write — Claude Skills 360 Blog
Blog / Testing / Claude Code Testing Strategy: Unit vs Integration vs E2E — What to Write
Testing

Claude Code Testing Strategy: Unit vs Integration vs E2E — What to Write

Published: June 14, 2026
Read time: 8 min read
By: Claude Skills 360

The most common testing mistake isn’t writing bad tests — it’s writing tests at the wrong level. Too many unit tests for integration concerns. E2E tests for logic that could be verified in milliseconds with a unit test. No tests at all for the database queries that actually fail in production. Claude Code writes better tests when you tell it why, not just what — and this guide explains the framework for making those decisions.

This guide covers testing strategy with Claude Code: when to write unit vs. integration vs. E2E tests, what to test, and avoiding false confidence from bad coverage.

The Testing Trophy

The testing trophy (Kent C. Dodds) is the mental model that best matches production failure patterns:

      /\
     /E2E\          — Few: user journeys, smoke tests
    /------\
   / Integr \       — Many: API routes, database queries, auth flows
  /----------\
 /   Unit     \     — Some: pure functions, algorithms, utilities
/--------------\
/    Static     \   — Always: types, linting, dead code

Most bugs happen at integration points — between your code and the database, between services, between components and their data. Unit tests catch logic bugs but miss integration failures. E2E tests catch everything but are slow and expensive to maintain.

Claude Code’s default testing strategy: write integration tests first, unit tests for complex logic, E2E tests for the paths that must never break.

CLAUDE.md for Testing

## Testing Strategy
- Framework: Vitest (unit/integration), Playwright (E2E)
- Database tests: real database, transaction rollback per test
- No mocks for: database, filesystem, crypto — use real implementations
- Mocks acceptable for: external APIs, email sending, payment processing
- Coverage target: 80% line coverage on src/api/ and src/lib/ — not all code
- E2E tests: login flow, checkout, critical user journeys only (< 20 tests)
- TDD for: complex business logic, state machines, data transformations
- Running tests: `npm test` (unit/integration), `npm run test:e2e` (Playwright)

What to Test at Each Level

Unit Tests — Pure Functions Only

Write unit tests for this discount calculation function.
// src/lib/pricing.ts
export function calculateDiscount(
  basePrice: number,
  customerTier: 'free' | 'pro' | 'enterprise',
  couponCode?: string,
): { discountPercent: number; finalPrice: number } {
  let discountPercent = 0;

  if (customerTier === 'pro') discountPercent = 10;
  if (customerTier === 'enterprise') discountPercent = 20;

  if (couponCode === 'SUMMER24') discountPercent = Math.max(discountPercent, 25);
  if (couponCode === 'WELCOME') discountPercent += 5; // Stackable

  const finalPrice = basePrice * (1 - discountPercent / 100);
  return { discountPercent, finalPrice };
}
// src/lib/pricing.test.ts
import { describe, it, expect } from 'vitest';
import { calculateDiscount } from './pricing';

describe('calculateDiscount', () => {
  it('returns no discount for free tier without coupon', () => {
    expect(calculateDiscount(100, 'free')).toEqual({
      discountPercent: 0,
      finalPrice: 100,
    });
  });

  it('applies tier discount for pro', () => {
    expect(calculateDiscount(100, 'pro')).toEqual({
      discountPercent: 10,
      finalPrice: 90,
    });
  });

  it('coupon overrides tier discount when higher', () => {
    expect(calculateDiscount(100, 'pro', 'SUMMER24')).toEqual({
      discountPercent: 25,
      finalPrice: 75,
    });
  });

  it('WELCOME coupon stacks with tier discount', () => {
    expect(calculateDiscount(100, 'pro', 'WELCOME')).toEqual({
      discountPercent: 15, // 10% tier + 5% WELCOME
      finalPrice: 85,
    });
  });
});

Unit tests are appropriate here: pure function, no I/O, complex enough logic to have edge cases worth testing.

Integration Tests — API Routes

Write integration tests for the POST /api/orders endpoint.
Test the happy path and critical failure cases.
// src/api/orders.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import { app } from '../app';
import { db } from '../lib/db';

// Wrap each test in a transaction that rolls back
const withTransaction = async (fn: (trx: any) => Promise<void>) => {
  const trx = await db.transaction();
  try {
    await fn(trx);
  } finally {
    await trx.rollback(); // Always rollback — tests are isolated
  }
};

describe('POST /api/orders', () => {
  let authToken: string;

  beforeEach(async () => {
    // Create test user and get token
    authToken = await createTestUser({ email: '[email protected]' });
  });

  it('creates order for authenticated user with valid items', async () => {
    const { productId } = await createTestProduct({ price: 2999, stock: 10 });

    const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        items: [{ productId, quantity: 2 }],
      });

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({
      orderId: expect.any(String),
      totalCents: 5998,
      status: 'pending',
    });

    // Verify it was persisted
    const order = await db('orders').where('id', response.body.orderId).first();
    expect(order).toBeDefined();
    expect(order.total_cents).toBe(5998);
  });

  it('returns 401 without auth token', async () => {
    const response = await request(app)
      .post('/api/orders')
      .send({ items: [{ productId: 'xxx', quantity: 1 }] });

    expect(response.status).toBe(401);
  });

  it('returns 400 for out-of-stock product', async () => {
    const { productId } = await createTestProduct({ price: 2999, stock: 0 });

    const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${authToken}`)
      .send({ items: [{ productId, quantity: 1 }] });

    expect(response.status).toBe(400);
    expect(response.body.error).toContain('out of stock');
  });

  it('is idempotent — same order not created twice', async () => {
    const { productId } = await createTestProduct({ stock: 10 });
    const idempotencyKey = 'order-key-123';

    const firstResponse = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${authToken}`)
      .set('Idempotency-Key', idempotencyKey)
      .send({ items: [{ productId, quantity: 1 }] });

    const secondResponse = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${authToken}`)
      .set('Idempotency-Key', idempotencyKey)
      .send({ items: [{ productId, quantity: 1 }] });

    expect(firstResponse.body.orderId).toBe(secondResponse.body.orderId);

    // Only one order in DB
    const orders = await db('orders').where('idempotency_key', idempotencyKey);
    expect(orders).toHaveLength(1);
  });
});

Integration tests hit the real database, test real HTTP behavior, and catch the failures that unit tests miss: SQL errors, constraint violations, auth middleware failures.

What NOT to Test

Do I need to test that Prisma's findUnique returns null when the record doesn't exist?

No. Testing framework internals and library behavior wastes time. Claude Code identifies tests that shouldn’t be written:

Don’t test:

  • That your ORM returns the data you asked for (library responsibility)
  • That parseInt('abc') returns NaN (language guarantee)
  • Getters/setters with no logic (return this.name)
  • Framework routing (test the handler, not that Express routes correctly)
  • Every possible null check on data you control

Do test:

  • Business rules (discount calculations, access control)
  • Request validation (what gets rejected, what gets through)
  • State transitions (order goes from pending → paid → shipped)
  • Error paths (what happens when the database is down, when payment fails)
  • Integration seams (your code + the database, your code + external API)

Test Coverage That Matters

We have 45% line coverage. The team wants to get to 80%.
What should I focus on to make coverage meaningful?

Claude Code analyzes which uncovered paths matter:

// Coverage report analysis prompt for Claude Code:
// "Look at the uncovered lines in our coverage report (paste report).
// Which uncovered paths represent real production failure risks?
// Which are untestable or not worth testing?"

// High-priority uncovered paths:
// - Error handling in payment processing
// - Authorization checks in API handlers
// - Database constraint handling
// - Retry logic in async operations

// Low-priority uncovered paths:
// - Logging statements
// - One-line getters
// - Environment-specific configuration
// - Test setup code

Coverage as a metric is lagging, not leading. 80% coverage with the wrong 80% gives false confidence. 60% coverage on the right paths — auth, payments, data mutations — is more valuable.

For specific testing patterns for each framework, see the testing and debugging guide for unit/integration patterns, and the Playwright E2E guide for end-to-end testing. For mobile testing, see the mobile testing guide. The Claude Skills 360 bundle includes testing strategy skill sets for test plan generation and coverage analysis. Start with the free tier to analyze and improve your testing strategy.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free