Claude Code Plugins

Community-maintained marketplace

Feedback

Comprehensive testing strategies including test pyramid, TDD methodology, testing patterns, coverage goals, and CI/CD integration. Use when writing tests, implementing TDD, reviewing test coverage, debugging test failures, or setting up testing infrastructure.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name testing-strategy
description Comprehensive testing strategies including test pyramid, TDD methodology, testing patterns, coverage goals, and CI/CD integration. Use when writing tests, implementing TDD, reviewing test coverage, debugging test failures, or setting up testing infrastructure.

Testing Strategy

This skill provides comprehensive guidance for implementing effective testing strategies across your entire application stack.

Test Pyramid

The Testing Hierarchy

        /\
       /  \
      /E2E \       10% - End-to-End Tests (slowest, most expensive)
     /______\
    /        \
   /Integration\  20% - Integration Tests (medium speed/cost)
  /____________\
 /              \
/   Unit Tests   \ 70% - Unit Tests (fast, cheap, focused)
/__________________\

Rationale:

  • 70% Unit Tests: Fast, isolated, catch bugs early
  • 20% Integration Tests: Test component interactions
  • 10% E2E Tests: Test critical user journeys

Why This Distribution?

Unit tests are cheap:

  • Run in milliseconds
  • No external dependencies
  • Easy to debug
  • High code coverage per test

Integration tests are moderate:

  • Test real interactions
  • Catch integration bugs
  • Slower than unit tests
  • More complex setup

E2E tests are expensive:

  • Test entire system
  • Catch UX issues
  • Very slow (seconds/minutes)
  • Brittle and hard to maintain

TDD (Test-Driven Development)

Red-Green-Refactor Cycle

1. Red - Write a failing test:

describe('Calculator', () => {
  test('adds two numbers', () => {
    const calculator = new Calculator();
    expect(calculator.add(2, 3)).toBe(5); // FAILS - method doesn't exist
  });
});

2. Green - Write minimal code to pass:

class Calculator {
  add(a: number, b: number): number {
    return a + b; // Simplest implementation
  }
}
// Test now PASSES

3. Refactor - Improve the code:

class Calculator {
  add(a: number, b: number): number {
    // Add validation
    if (!Number.isFinite(a) || !Number.isFinite(b)) {
      throw new Error('Arguments must be finite numbers');
    }
    return a + b;
  }
}

TDD Benefits

Design benefits:

  • Forces you to think about API before implementation
  • Leads to more testable, modular code
  • Encourages SOLID principles

Quality benefits:

  • 100% test coverage by design
  • Catches bugs immediately
  • Provides living documentation

Workflow benefits:

  • Clear next step (make test pass)
  • Confidence when refactoring
  • Prevents over-engineering

Arrange-Act-Assert Pattern

The AAA Pattern

Every test should follow this structure:

test('user registration creates account and sends welcome email', async () => {
  // ARRANGE - Set up test conditions
  const userData = {
    email: 'test@example.com',
    password: 'SecurePass123',
    name: 'Test User',
  };
  const mockEmailService = jest.fn();
  const userService = new UserService(mockEmailService);

  // ACT - Execute the behavior being tested
  const result = await userService.register(userData);

  // ASSERT - Verify the outcome
  expect(result.id).toBeDefined();
  expect(result.email).toBe(userData.email);
  expect(mockEmailService).toHaveBeenCalledWith({
    to: userData.email,
    subject: 'Welcome!',
    template: 'welcome',
  });
});

Why AAA?

  • Clear structure: Easy to understand what's being tested
  • Consistent: All tests follow same pattern
  • Maintainable: Easy to modify and debug

Mocking Strategies

When to Mock

✅ DO mock:

  • External APIs
  • Databases
  • File system operations
  • Time/dates
  • Random number generators
  • Network requests
  • Third-party services
// Mock external API
jest.mock('axios');

test('fetches user data from API', async () => {
  const mockData = { id: 1, name: 'John' };
  (axios.get as jest.Mock).mockResolvedValue({ data: mockData });

  const user = await fetchUser(1);

  expect(user).toEqual(mockData);
});

When NOT to Mock

❌ DON'T mock:

  • Pure functions (test them directly)
  • Simple utility functions
  • Domain logic
  • Value objects
  • Internal implementation details
// ❌ BAD - Over-mocking
test('validates email', () => {
  const validator = new EmailValidator();
  jest.spyOn(validator, 'isValid').mockReturnValue(true);
  expect(validator.isValid('test@example.com')).toBe(true);
  // This test is useless - you're testing the mock, not the code
});

// ✅ GOOD - Test real implementation
test('validates email', () => {
  const validator = new EmailValidator();
  expect(validator.isValid('test@example.com')).toBe(true);
  expect(validator.isValid('invalid')).toBe(false);
});

Mocking Patterns

Stub (return predetermined values):

const mockDatabase = {
  findUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' }),
  saveUser: jest.fn().mockResolvedValue(true),
};

Spy (track calls, use real implementation):

const emailService = new EmailService();
const sendSpy = jest.spyOn(emailService, 'send');

await emailService.send('test@example.com', 'Hello');

expect(sendSpy).toHaveBeenCalledTimes(1);
expect(sendSpy).toHaveBeenCalledWith('test@example.com', 'Hello');

Fake (lightweight implementation):

class FakeDatabase {
  private data = new Map();

  async save(key: string, value: any) {
    this.data.set(key, value);
  }

  async get(key: string) {
    return this.data.get(key);
  }
}

Test Coverage Goals

Coverage Metrics

Line Coverage: Percentage of code lines executed

  • Target: 80-90% for critical paths

Branch Coverage: Percentage of if/else branches tested

  • Target: 80%+ (more important than line coverage)

Function Coverage: Percentage of functions called

  • Target: 90%+

Statement Coverage: Percentage of statements executed

  • Target: 80%+

Coverage Configuration

// package.json
{
  "jest": {
    "collectCoverage": true,
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 90,
        "lines": 80,
        "statements": 80
      },
      "./src/critical/": {
        "branches": 95,
        "functions": 95,
        "lines": 95,
        "statements": 95
      }
    },
    "coveragePathIgnorePatterns": [
      "/node_modules/",
      "/tests/",
      "/migrations/",
      "/.config.ts$/"
    ]
  }
}

What to Prioritize

High priority (aim for 95%+ coverage):

  • Business logic
  • Security-critical code
  • Payment/billing code
  • Data validation
  • Authentication/authorization

Medium priority (aim for 80%+ coverage):

  • API endpoints
  • Database queries
  • Utility functions
  • Error handling

Low priority (optional coverage):

  • UI components (use integration tests instead)
  • Configuration files
  • Type definitions
  • Third-party library wrappers

Integration Testing

Database Integration Tests

import { PrismaClient } from '@prisma/client';

describe('UserRepository', () => {
  let prisma: PrismaClient;
  let repository: UserRepository;

  beforeAll(async () => {
    // Use test database
    prisma = new PrismaClient({
      datasources: { db: { url: process.env.TEST_DATABASE_URL } },
    });
    repository = new UserRepository(prisma);
  });

  beforeEach(async () => {
    // Clean database before each test
    await prisma.user.deleteMany();
  });

  afterAll(async () => {
    await prisma.$disconnect();
  });

  test('creates user and retrieves by email', async () => {
    // ARRANGE
    const userData = {
      email: 'test@example.com',
      name: 'Test User',
      password: 'hashed_password',
    };

    // ACT
    const created = await repository.create(userData);
    const retrieved = await repository.findByEmail(userData.email);

    // ASSERT
    expect(retrieved).toBeDefined();
    expect(retrieved?.id).toBe(created.id);
    expect(retrieved?.email).toBe(userData.email);
  });
});

API Integration Tests

import request from 'supertest';
import { app } from '../src/app';

describe('User API', () => {
  test('POST /api/users creates user and returns 201', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123',
        name: 'Test User',
      })
      .expect(201);

    expect(response.body).toMatchObject({
      email: 'test@example.com',
      name: 'Test User',
    });
    expect(response.body.password).toBeUndefined(); // Never return password
  });

  test('POST /api/users returns 400 for invalid email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        email: 'invalid-email',
        password: 'SecurePass123',
        name: 'Test User',
      })
      .expect(400);

    expect(response.body.error.code).toBe('VALIDATION_ERROR');
  });
});

Service Integration Tests

describe('OrderService Integration', () => {
  test('complete order flow', async () => {
    // Create order
    const order = await orderService.create({
      userId: 'user_123',
      items: [{ productId: 'prod_1', quantity: 2 }],
    });

    // Process payment
    const payment = await paymentService.process({
      orderId: order.id,
      amount: order.total,
    });

    // Verify inventory updated
    const product = await inventoryService.getProduct('prod_1');
    expect(product.stock).toBe(originalStock - 2);

    // Verify order status updated
    const updatedOrder = await orderService.getById(order.id);
    expect(updatedOrder.status).toBe('paid');
  });
});

E2E Testing

Playwright Setup

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
});

E2E Test Example

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

test.describe('User Registration Flow', () => {
  test('user can register and login', async ({ page }) => {
    // Navigate to registration page
    await page.goto('/register');

    // Fill registration form
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'SecurePass123');
    await page.fill('[name="confirmPassword"]', 'SecurePass123');
    await page.fill('[name="name"]', 'Test User');

    // Submit form
    await page.click('button[type="submit"]');

    // Wait for redirect to dashboard
    await page.waitForURL('/dashboard');

    // Verify welcome message
    await expect(page.locator('h1')).toContainText('Welcome, Test User');
  });

  test('shows validation errors for invalid input', async ({ page }) => {
    await page.goto('/register');

    await page.fill('[name="email"]', 'invalid-email');
    await page.fill('[name="password"]', '123'); // Too short

    await page.click('button[type="submit"]');

    // Verify error messages displayed
    await expect(page.locator('[data-testid="email-error"]'))
      .toContainText('Invalid email');
    await expect(page.locator('[data-testid="password-error"]'))
      .toContainText('at least 8 characters');
  });
});

Critical E2E Scenarios

Test these critical user journeys:

  • User registration and login
  • Checkout and payment flow
  • Password reset
  • Profile updates
  • Critical business workflows

Performance Testing

Load Testing with k6

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 20 },   // Ramp up to 20 users
    { duration: '1m', target: 20 },    // Stay at 20 users
    { duration: '30s', target: 100 },  // Ramp up to 100 users
    { duration: '1m', target: 100 },   // Stay at 100 users
    { duration: '30s', target: 0 },    // Ramp down to 0 users
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
    http_req_failed: ['rate<0.01'],   // Less than 1% error rate
  },
};

export default function() {
  const response = http.get('https://api.example.com/users');

  check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}

Benchmark Testing

import { performance } from 'perf_hooks';

describe('Performance Benchmarks', () => {
  test('database query completes in under 100ms', async () => {
    const start = performance.now();

    await database.query('SELECT * FROM users WHERE email = ?', ['test@example.com']);

    const duration = performance.now() - start;
    expect(duration).toBeLessThan(100);
  });

  test('API endpoint responds in under 200ms', async () => {
    const start = performance.now();

    await request(app).get('/api/users/123');

    const duration = performance.now() - start;
    expect(duration).toBeLessThan(200);
  });
});

Flaky Test Prevention

Common Causes of Flaky Tests

1. Race Conditions:

// ❌ BAD - Race condition
test('displays data', async () => {
  fetchData();
  expect(screen.getByText('Data loaded')).toBeInTheDocument();
  // Fails intermittently if fetchData takes longer than expected
});

// ✅ GOOD - Wait for async operation
test('displays data', async () => {
  fetchData();
  await screen.findByText('Data loaded'); // Waits up to 1 second
});

2. Time Dependencies:

// ❌ BAD - Depends on current time
test('shows message for new users', () => {
  const user = { createdAt: new Date() };
  expect(isNewUser(user)).toBe(true);
  // Fails if test runs slowly
});

// ✅ GOOD - Mock time
test('shows message for new users', () => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2025-10-16'));

  const user = { createdAt: new Date('2025-10-15') };
  expect(isNewUser(user)).toBe(true);

  jest.useRealTimers();
});

3. Shared State:

// ❌ BAD - Tests share state
let counter = 0;

test('increments counter', () => {
  counter++;
  expect(counter).toBe(1);
});

test('increments counter again', () => {
  counter++;
  expect(counter).toBe(1); // Fails if first test ran
});

// ✅ GOOD - Isolated state
test('increments counter', () => {
  const counter = new Counter();
  counter.increment();
  expect(counter.value).toBe(1);
});

Flaky Test Best Practices

  1. Always clean up after tests:
afterEach(async () => {
  await database.truncate();
  jest.clearAllMocks();
  jest.useRealTimers();
});
  1. Use explicit waits, not delays:
// ❌ BAD
await sleep(1000);

// ✅ GOOD
await waitFor(() => expect(element).toBeInTheDocument());
  1. Isolate test data:
test('creates user', async () => {
  const uniqueEmail = `test-${Date.now()}@example.com`;
  const user = await createUser({ email: uniqueEmail });
  expect(user.email).toBe(uniqueEmail);
});

Test Data Management

Test Fixtures

// fixtures/users.ts
export const testUsers = {
  admin: {
    email: 'admin@example.com',
    password: 'AdminPass123',
    role: 'admin',
  },
  regular: {
    email: 'user@example.com',
    password: 'UserPass123',
    role: 'user',
  },
};

// Usage in tests
import { testUsers } from './fixtures/users';

test('admin can delete users', async () => {
  const admin = await createUser(testUsers.admin);
  // Test admin functionality
});

Factory Pattern

class UserFactory {
  static create(overrides = {}) {
    return {
      id: faker.datatype.uuid(),
      email: faker.internet.email(),
      name: faker.name.fullName(),
      createdAt: new Date(),
      ...overrides,
    };
  }

  static createMany(count: number, overrides = {}) {
    return Array.from({ length: count }, () => this.create(overrides));
  }
}

// Usage
test('displays user list', () => {
  const users = UserFactory.createMany(5);
  render(<UserList users={users} />);
  expect(screen.getAllByRole('listitem')).toHaveLength(5);
});

Database Seeding

// seeds/test-seed.ts
export async function seedTestDatabase() {
  // Create admin user
  const admin = await prisma.user.create({
    data: { email: 'admin@test.com', role: 'admin' },
  });

  // Create test products
  const products = await Promise.all([
    prisma.product.create({ data: { name: 'Product 1', price: 10 } }),
    prisma.product.create({ data: { name: 'Product 2', price: 20 } }),
  ]);

  return { admin, products };
}

// Usage
beforeEach(async () => {
  await prisma.$executeRaw`TRUNCATE TABLE users CASCADE`;
  const { admin, products } = await seedTestDatabase();
});

CI/CD Integration

GitHub Actions Configuration

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run type check
        run: npm run type-check

      - name: Run unit tests
        run: npm run test:unit

      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test

      - name: Run E2E tests
        run: npm run test:e2e

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json
          fail_ci_if_error: true

Test Scripts Organization

// package.json
{
  "scripts": {
    "test": "npm run test:unit && npm run test:integration && npm run test:e2e",
    "test:unit": "jest --testPathPattern=\\.test\\.ts$",
    "test:integration": "jest --testPathPattern=\\.integration\\.ts$",
    "test:e2e": "playwright test",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --maxWorkers=2"
  }
}

Test Performance in CI

Parallel execution:

jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - run: npm test -- --shard=${{ matrix.shard }}/4

Cache dependencies:

- uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

Test Organization

File Structure

tests/
├── unit/                   # Fast, isolated tests
│   ├── services/
│   │   ├── user-service.test.ts
│   │   └── order-service.test.ts
│   └── utils/
│       ├── validator.test.ts
│       └── formatter.test.ts
├── integration/            # Database, API tests
│   ├── api/
│   │   ├── users.integration.ts
│   │   └── orders.integration.ts
│   └── database/
│       └── repository.integration.ts
├── e2e/                   # End-to-end tests
│   ├── auth.spec.ts
│   ├── checkout.spec.ts
│   └── profile.spec.ts
├── fixtures/              # Test data
│   ├── users.ts
│   └── products.ts
└── helpers/               # Test utilities
    ├── setup.ts
    └── factories.ts

Test Naming Conventions

// Pattern: describe('Component/Function', () => test('should...when...'))

describe('UserService', () => {
  describe('register', () => {
    test('should create user when valid data provided', async () => {
      // Test implementation
    });

    test('should throw error when email already exists', async () => {
      // Test implementation
    });

    test('should hash password before saving', async () => {
      // Test implementation
    });
  });

  describe('login', () => {
    test('should return token when credentials are valid', async () => {
      // Test implementation
    });

    test('should throw error when password is incorrect', async () => {
      // Test implementation
    });
  });
});

When to Use This Skill

Use this skill when:

  • Setting up testing infrastructure
  • Writing unit, integration, or E2E tests
  • Implementing TDD methodology
  • Reviewing test coverage
  • Debugging flaky tests
  • Optimizing test performance
  • Configuring CI/CD pipelines
  • Establishing testing standards
  • Training team on testing practices
  • Improving code quality through testing

Remember: Good tests give you confidence to refactor, catch bugs early, and serve as living documentation. Invest in your test suite and it will pay dividends throughout the project lifecycle.