APM

>Agent Skill

@thamjiahe/testing-strategy

skilldesign

Apply TDD with RED-GREEN-REFACTOR cycles, separate unit tests from integration tests, ensure comprehensive coverage. Apply when writing tests, evaluating test coverage, testing databases, or testing admin flows.

testingapi-designperformancesecurity
Claude Opus 4.5, Claude Code v2.x
apm::install
$apm install @thamjiahe/testing-strategy
apm::allowed-tools
ReadWriteEditBash
apm::skill.md
---
name: "Testing Strategy"
description: "Apply TDD with RED-GREEN-REFACTOR cycles, separate unit tests from integration tests, ensure comprehensive coverage. Apply when writing tests, evaluating test coverage, testing databases, or testing admin flows."
allowed-tools: Read, Write, Edit, Bash
version: 2.1.0
compatibility: Claude Opus 4.5, Claude Code v2.x
updated: 2026-01-24
---

# Testing Strategy

Systematic TDD workflow ensuring comprehensive test coverage following RED-GREEN-REFACTOR cycles.

## Overview

This Skill enforces:
- RED-GREEN-REFACTOR cycles (TDD)
- Atomic test coverage
- Separation of logic from database tests (T-3)
- E2E testing for critical admin flows (T-7)
- Edge case coverage (T-8)

Apply when writing tests, designing test suites, or evaluating coverage.

## RED-GREEN-REFACTOR Workflow

**Every feature follows this cycle**:

### RED Phase: Write Failing Test

Write test BEFORE implementation:

```ts
import { describe, test, expect } from 'vitest';
import { validateEmail } from './email';

describe('validateEmail', () => {
  test('returns true for valid email', () => {
    expect(validateEmail('user@example.com')).toBe(true);
  });

  test('returns false for missing @', () => {
    expect(validateEmail('userexample.com')).toBe(false);
  });

  test('returns false for empty string', () => {
    expect(validateEmail('')).toBe(false);
  });
});
```

Run: `pnpm test validateEmail`**FAILS** (RED)

### GREEN Phase: Make Test Pass

Write minimal code to pass:

```ts
export function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
```

Run: `pnpm test validateEmail`**PASSES** (GREEN)

### REFACTOR Phase: Improve Code

Improve without changing behavior:

```ts
// Extract pattern for readability
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export function validateEmail(email: string): boolean {
  return EMAIL_PATTERN.test(email);
}
```

Run: `pnpm test validateEmail`**STILL PASSES** (verify before claiming done)

## Test Organization

### T-1 (MUST): Colocate Tests with Source

```
src/utils/validators.ts
src/utils/validators.spec.ts      ← Same directory
```

### T-3 (MUST): Separate Logic from Database Tests

**Unit Tests** (pure logic, no database):

```ts
// src/utils/helpers.spec.ts
describe('calculateTotal', () => {
  test('sums array correctly', () => {
    const result = calculateTotal([10, 20, 30]);
    expect(result).toBe(60);
  });

  test('handles empty array', () => {
    expect(calculateTotal([])).toBe(0);
  });
});
```

**Integration Tests** (with database):

```ts
// server/tests/user-api.test.ts
describe('User API', () => {
  beforeEach(async () => {
    await db.clear('users');
  });

  test('creates user in database', async () => {
    const user = await createUser({
      email: 'test@example.com',
      name: 'Test User'
    });

    const retrieved = await db.users.findById(user.id);
    expect(retrieved).toEqual(user);
  });
});
```

### Anti-Pattern: Mixed Tests

```ts
// ❌ BAD: Mixes logic and database
describe('calculateTotal', () => {
  test('calculates and saves', async () => {
    const result = calculateTotal([10, 20, 30]);
    await db.totals.save(result);  // Don't mix!
    expect(result).toBe(60);
  });
});
```

## Test Coverage Requirements

**By Feature Type**:

- **Utilities** (formatting, validation): 80%+ coverage
- **Business Logic** (algorithms, rules): 90%+ coverage
- **Admin Flows** (user management): 100% coverage (T-7)
- **Public APIs** (REST endpoints): 90%+ coverage

Check coverage:

```bash
pnpm test --coverage
```

## Unit Test Patterns

### Pattern 1: Simple Function

```ts
// ✅ GOOD: Complete test
test('returns true for valid email format', () => {
  expect(validateEmail('user@example.com')).toBe(true);
});

// ❌ BAD: Unclear what's being tested
test('validates email', () => {
  expect(validateEmail('user@example.com')).toBe(true);
});
```

### Pattern 2: Edge Cases (T-8)

```ts
// ✅ GOOD: Covers boundaries
describe('calculateDiscount', () => {
  test('returns 0% for purchases under $100', () => {
    expect(calculateDiscount(99.99)).toBe(0);
  });

  test('returns 10% for purchases >= $100', () => {
    expect(calculateDiscount(100)).toBe(10);
    expect(calculateDiscount(100.01)).toBe(10.001);
  });

  test('handles edge cases', () => {
    expect(calculateDiscount(0)).toBe(0);      // Zero
    expect(calculateDiscount(-50)).toBe(0);    // Negative
    expect(calculateDiscount(999999)).toBe(99999.9);  // Large
  });
});
```

### Pattern 3: Parameterized Tests

```ts
// ✅ GOOD: No magic literals
test.each([
  ['user@example.com', true],
  ['invalid.email', false],
  ['', false],
  ['user@domain.co.uk', true]
])('validateEmail("%s") returns %p', (email, expected) => {
  expect(validateEmail(email)).toBe(expected);
});
```

### Pattern 4: Entire Structure Assertion

**T-1 (MUST)**: Compare entire result, not individual fields:

```ts
// ✅ GOOD: Complete structure
const result = createUser({ name: 'Alice', email: 'alice@example.com' });
expect(result).toEqual({
  id: expect.any(String),
  name: 'Alice',
  email: 'alice@example.com',
  createdAt: expect.any(Date)
});

// ❌ BAD: Separate assertions
expect(result).toHaveProperty('id');
expect(result.name).toBe('Alice');
expect(result.email).toBe('alice@example.com');
```

## Anti-Patterns

Avoid these:

```ts
// ❌ Testing implementation details
test('caches value internally', () => {
  const cache = getInternalCache();
  expect(cache).toContain('value');
});

// ❌ Trivial assertions
test('2 equals 2', () => {
  expect(2).toBe(2);
});

// ❌ Magic numbers
test('total calculation', () => {
  expect(calculateTotal([10, 20, 30])).toBe(60);
  // What do 10, 20, 30 represent?
});

// ❌ Testing type checker conditions
test('rejects null', () => {
  // @ts-expect-error - Testing invalid input
  expect(validateEmail(null)).toBe(false);
});

// ❌ Mixing async and sync confusingly
test('async function', () => {
  const result = fetchUser('123');
  expect(result).toBe(user);  // Wrong! result is Promise
});
```

## Integration Test Patterns

### Testing APIs

```ts
describe('POST /api/users', () => {
  test('creates user with valid input', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: 'alice@example.com' })
      .expect(201);

    expect(response.body).toEqual({
      id: expect.any(String),
      name: 'Alice',
      email: 'alice@example.com'
    });
  });

  test('returns 400 for missing required fields', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Alice' })
      .expect(400);

    expect(response.body.error).toContain('Email required');
  });

  test('returns 409 for duplicate email', async () => {
    await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: 'alice@example.com' });

    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Bob', email: 'alice@example.com' })
      .expect(409);

    expect(response.body.error).toContain('already exists');
  });
});
```

### Testing Database Operations

```ts
describe('User model', () => {
  beforeEach(async () => {
    await db.connect();
    await db.clear('users');
  });

  afterEach(async () => {
    await db.disconnect();
  });

  test('creates and retrieves user', async () => {
    const user = await User.create({
      name: 'Alice',
      email: 'alice@example.com'
    });

    const retrieved = await User.findById(user.id);
    expect(retrieved).toEqual(user);
  });

  test('enforces unique email constraint', async () => {
    await User.create({ name: 'Alice', email: 'alice@example.com' });

    await expect(
      User.create({ name: 'Bob', email: 'alice@example.com' })
    ).rejects.toThrow('Unique constraint');
  });
});
```

## E2E Test Patterns

### Critical Admin Flows (T-7)

E2E test all critical admin workflows:

```ts
import { test, expect } from '@playwright/test';

test.describe('Admin User Management', () => {
  test.beforeEach(async ({ page }) => {
    // Login as admin
    await page.goto('/login');
    await page.fill('input[name="email"]', 'admin@company.com');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button:has-text("Login")');
    await page.waitForURL('/admin/dashboard');
  });

  test('creates new user', async ({ page }) => {
    await page.click('a:has-text("Users")');
    await page.click('button:has-text("New User")');
    await page.fill('input[name="name"]', 'John Doe');
    await page.fill('input[name="email"]', 'john@company.com');
    await page.click('button:has-text("Create")');

    await page.waitForSelector('text=User created');
    await expect(page).toContainText('john@company.com');
  });

  test('deletes user with confirmation', async ({ page }) => {
    await page.click('a:has-text("Users")');
    await page.click('[data-test="delete-btn"]');

    // Must require confirmation (U-5)
    await expect(page).toContainText('Are you sure?');
    await page.click('button:has-text("Confirm")');

    await page.waitForSelector('text=User deleted');
  });

  test('prevents accidental deletion', async ({ page }) => {
    await page.click('a:has-text("Users")');
    await page.click('[data-test="delete-btn"]');
    await page.click('button:has-text("Cancel")');

    // User should still exist
    await expect(page).not.toContainText('User deleted');
  });
});
```

## Verification Before Completion

Before marking tests complete:

- [ ] **RED phase**: Watched tests fail first
- [ ] **GREEN phase**: Tests pass with minimal code
- [ ] **REFACTOR phase**: Improved code quality
- [ ] **Verify again**: All tests still pass
- [ ] Edge cases covered (null, empty, zero, negative, large values)
- [ ] Pure logic separated from database operations
- [ ] Coverage meets minimum requirements
- [ ] No trivial assertions (avoid `expect(true).toBe(true)`)
- [ ] Tests colocated with source code
- [ ] E2E tests for critical admin flows

## Running Tests

```bash
# All tests
pnpm test

# Watch mode (rerun on change)
pnpm test --watch

# Specific file
pnpm test src/utils/helpers.spec.ts

# Coverage report
pnpm test --coverage

# Verbose output
pnpm test --reporter=verbose
```

## Integration with CLAUDE.md

Enforces CLAUDE.md Section 3:
- **T-1**: Tests colocated with source
- **T-2**: API changes have integration tests
- **T-3**: Separate logic from database tests
- **T-7**: E2E tests for admin flows
- **T-8**: Edge cases tested
- **T-9**: Redundant tests better than missing coverage
- **T-10**: RED-GREEN-REFACTOR cycle
---

**Last Updated:** January 24, 2026
**Compatibility:** Claude Opus 4.5, Claude Code v2.x
**Status:** Production Ready

> **January 2026 Update:** This skill is compatible with Claude Opus 4.5 and Claude Code v2.x. For complex tasks, use the `effort: high` parameter for thorough analysis.