skills/claudeforge/orchestrator/testing-strategies

testing-strategies

SKILL.md

Testing Strategies Skill

Comprehensive testing strategies and patterns for ensuring code quality.

Testing Pyramid

              /\
             /E2E\           5%  - Critical user journeys
            /------\
           / Integ  \        15% - Service boundaries
          /----------\
         /    Unit    \      80% - Component logic
        /--------------\

Unit Testing Patterns

Arrange-Act-Assert (AAA)

describe('calculateTotal', () => {
  it('should apply discount correctly', () => {
    // Arrange
    const items = [{ price: 100, quantity: 1 }];
    const discount = 0.1;

    // Act
    const result = calculateTotal(items, discount);

    // Assert
    expect(result).toBe(90);
  });
});

Test Isolation

describe('UserService', () => {
  let service: UserService;
  let mockRepo: MockUserRepository;

  beforeEach(() => {
    // Fresh instances for each test
    mockRepo = new MockUserRepository();
    service = new UserService(mockRepo);
  });

  afterEach(() => {
    vi.clearAllMocks();
  });
});

Parameterized Tests

describe('validateEmail', () => {
  it.each([
    ['test@example.com', true],
    ['user.name@domain.co.uk', true],
    ['invalid', false],
    ['@nodomain.com', false],
    ['no@tld', false],
  ])('validates %s as %s', (email, expected) => {
    expect(validateEmail(email)).toBe(expected);
  });
});

Testing Edge Cases

describe('divide', () => {
  it('should handle positive numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  it('should handle negative numbers', () => {
    expect(divide(-10, 2)).toBe(-5);
  });

  it('should handle zero dividend', () => {
    expect(divide(0, 5)).toBe(0);
  });

  it('should throw on division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });

  it('should handle floating point', () => {
    expect(divide(1, 3)).toBeCloseTo(0.333, 2);
  });
});

Component Testing

React Testing Library

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('LoginForm', () => {
  const mockOnSubmit = vi.fn();

  beforeEach(() => {
    mockOnSubmit.mockClear();
  });

  it('submits with valid data', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.type(screen.getByLabelText(/email/i), 'test@example.com');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123',
      });
    });
  });

  it('shows validation errors', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });
});

Testing Hooks

import { renderHook, act, waitFor } from '@testing-library/react';

describe('useCounter', () => {
  it('increments counter', () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });
});

describe('useAsync', () => {
  it('handles async operation', async () => {
    const mockFetch = vi.fn().mockResolvedValue({ data: 'test' });

    const { result } = renderHook(() => useAsync(mockFetch));

    expect(result.current.isLoading).toBe(true);

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    expect(result.current.data).toEqual({ data: 'test' });
  });
});

Integration Testing

API Testing with Hono

import { testClient } from 'hono/testing';
import app from '../src/index';

describe('Users API', () => {
  const client = testClient(app);

  beforeEach(async () => {
    await db.delete(users);
  });

  it('creates and retrieves user', async () => {
    // Create
    const createRes = await client.api.v1.users.$post({
      json: { name: 'Test', email: 'test@example.com', password: 'pass123' },
    });
    expect(createRes.status).toBe(201);
    const created = await createRes.json();

    // Retrieve
    const getRes = await client.api.v1.users[':id'].$get({
      param: { id: created.data.id },
    });
    expect(getRes.status).toBe(200);
    const retrieved = await getRes.json();

    expect(retrieved.data.email).toBe('test@example.com');
  });
});

Database Testing

describe('UserRepository', () => {
  const repo = new UserRepository();

  beforeEach(async () => {
    await db.delete(users);
  });

  it('creates and finds user', async () => {
    const created = await repo.create({
      name: 'Test User',
      email: 'test@example.com',
    });

    const found = await repo.findById(created.id);

    expect(found).toEqual(created);
  });

  it('returns null for non-existent user', async () => {
    const found = await repo.findById('non-existent-id');
    expect(found).toBeNull();
  });
});

E2E Testing with Playwright

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

test.describe('User Flow', () => {
  test('complete registration and login flow', async ({ page }) => {
    // Register
    await page.goto('/register');
    await page.fill('[name="email"]', 'new@example.com');
    await page.fill('[name="password"]', 'Password123!');
    await page.click('button[type="submit"]');

    // Verify redirect to login
    await expect(page).toHaveURL('/login');

    // Login
    await page.fill('[name="email"]', 'new@example.com');
    await page.fill('[name="password"]', 'Password123!');
    await page.click('button[type="submit"]');

    // Verify dashboard access
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('h1')).toContainText('Dashboard');
  });
});

Mocking Strategies

MSW (Mock Service Worker)

import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json({
      success: true,
      data: [{ id: '1', name: 'Test User' }],
    });
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json(
      { success: true, data: { id: '2', ...body } },
      { status: 201 }
    );
  }),
];

export const server = setupServer(...handlers);

// Setup in test file
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Vitest Mocks

// Mock module
vi.mock('./api', () => ({
  fetchUsers: vi.fn().mockResolvedValue([{ id: '1', name: 'Test' }]),
}));

// Mock implementation
const mockFetch = vi.fn();
mockFetch
  .mockResolvedValueOnce({ data: 'first' })
  .mockResolvedValueOnce({ data: 'second' });

// Spy on method
const spy = vi.spyOn(console, 'log');
expect(spy).toHaveBeenCalledWith('message');

Coverage Goals

Metric Minimum Target
Statements 70% 85%
Branches 65% 80%
Functions 70% 85%
Lines 70% 85%

Test Organization

tests/
├── unit/              # Unit tests
│   ├── utils/
│   └── services/
├── integration/       # Integration tests
│   ├── api/
│   └── db/
├── e2e/              # E2E tests
│   ├── auth.spec.ts
│   └── dashboard.spec.ts
├── fixtures/         # Test data
├── mocks/            # Mock implementations
└── setup.ts          # Global setup
Weekly Installs
3
GitHub Stars
36
First Seen
11 days ago
Installed on
opencode3
github-copilot3
codex3
amp3
cline3
kimi-cli3