Skip to main content

Agent Skills Framework Extension

Testing Strategies Skill

When to Use This Skill

Use this skill when implementing testing strategies patterns in your codebase.

How to Use This Skill

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

TDD, unit testing, integration testing, mocking, and test automation patterns for high-quality codebases.

Core Capabilities

  1. TDD Workflow - Red-green-refactor cycle
  2. Unit Testing - Isolated component tests
  3. Integration Testing - System interaction tests
  4. Mocking - Test doubles, MSW, dependency injection
  5. Coverage - Meaningful coverage strategies

TDD Workflow

Red-Green-Refactor

// 1. RED: Write a failing test first
// tests/user.service.test.ts

import { describe, it, expect, beforeEach } from 'vitest';
import { UserService } from '../src/services/user.service';
import { MockUserRepository } from './mocks/user.repository.mock';

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

beforeEach(() => {
mockRepo = new MockUserRepository();
userService = new UserService(mockRepo);
});

describe('createUser', () => {
it('should create a user with hashed password', async () => {
// Arrange
const userData = {
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User',
};

// Act
const user = await userService.createUser(userData);

// Assert
expect(user.email).toBe(userData.email);
expect(user.name).toBe(userData.name);
expect(user.password).not.toBe(userData.password); // Hashed
expect(user.id).toBeDefined();
});

it('should throw ValidationError for weak password', async () => {
const userData = {
email: 'test@example.com',
password: '123', // Too weak
name: 'Test User',
};

await expect(userService.createUser(userData))
.rejects.toThrow('Password must be at least 8 characters');
});

it('should throw ConflictError for duplicate email', async () => {
const userData = {
email: 'existing@example.com',
password: 'SecurePass123!',
name: 'Test User',
};

mockRepo.setExistingUser({ id: '1', email: userData.email });

await expect(userService.createUser(userData))
.rejects.toThrow('Email already registered');
});
});
});

// 2. GREEN: Write minimal code to pass
// src/services/user.service.ts

import { hash } from 'bcrypt';
import { randomUUID } from 'crypto';

interface CreateUserDTO {
email: string;
password: string;
name: string;
}

interface User {
id: string;
email: string;
password: string;
name: string;
}

export class UserService {
constructor(private readonly userRepo: UserRepository) {}

async createUser(data: CreateUserDTO): Promise<User> {
// Validate password
if (data.password.length < 8) {
throw new Error('Password must be at least 8 characters');
}

// Check for existing user
const existing = await this.userRepo.findByEmail(data.email);
if (existing) {
throw new Error('Email already registered');
}

// Hash password
const hashedPassword = await hash(data.password, 10);

// Create user
const user: User = {
id: randomUUID(),
email: data.email,
password: hashedPassword,
name: data.name,
};

await this.userRepo.save(user);
return user;
}
}

// 3. REFACTOR: Improve code quality
// Extract validation, use proper error types, etc.

Unit Testing Patterns

AAA Pattern (Arrange-Act-Assert)

// tests/calculator.test.ts
import { describe, it, expect } from 'vitest';
import { Calculator } from '../src/calculator';

describe('Calculator', () => {
describe('add', () => {
it('should add two positive numbers', () => {
// Arrange
const calculator = new Calculator();
const a = 2;
const b = 3;

// Act
const result = calculator.add(a, b);

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

it('should handle negative numbers', () => {
// Arrange
const calculator = new Calculator();

// Act
const result = calculator.add(-5, 3);

// Assert
expect(result).toBe(-2);
});
});

describe('divide', () => {
it('should divide two numbers', () => {
const calculator = new Calculator();
expect(calculator.divide(10, 2)).toBe(5);
});

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

Testing Async Code

// tests/api.test.ts
import { describe, it, expect, vi } from 'vitest';
import { ApiClient } from '../src/api-client';

describe('ApiClient', () => {
it('should fetch user data', async () => {
// Mock fetch
const mockUser = { id: '1', name: 'John' };
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUser),
});

const client = new ApiClient('https://api.example.com');
const user = await client.getUser('1');

expect(user).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith(
'https://api.example.com/users/1',
expect.objectContaining({ method: 'GET' })
);
});

it('should handle API errors', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
});

const client = new ApiClient('https://api.example.com');

await expect(client.getUser('999')).rejects.toThrow('Not Found');
});

it('should handle network errors', async () => {
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));

const client = new ApiClient('https://api.example.com');

await expect(client.getUser('1')).rejects.toThrow('Network error');
});
});

Mocking Patterns

Mock Service Worker (MSW)

// tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'John', email: 'john@example.com' },
{ id: '2', name: 'Jane', email: 'jane@example.com' },
]);
}),

http.get('/api/users/:id', ({ params }) => {
const { id } = params;

if (id === '404') {
return new HttpResponse(null, { status: 404 });
}

return HttpResponse.json({
id,
name: 'John',
email: 'john@example.com',
});
}),

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

http.delete('/api/users/:id', () => {
return new HttpResponse(null, { status: 204 });
}),
];

// tests/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

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

// tests/setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './mocks/server';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// tests/user-list.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { server } from './mocks/server';
import { UserList } from '../src/components/UserList';

describe('UserList', () => {
it('should display users from API', async () => {
render(<UserList />);

await waitFor(() => {
expect(screen.getByText('John')).toBeInTheDocument();
expect(screen.getByText('Jane')).toBeInTheDocument();
});
});

it('should display error on API failure', async () => {
// Override handler for this test
server.use(
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 });
})
);

render(<UserList />);

await waitFor(() => {
expect(screen.getByText(/error loading users/i)).toBeInTheDocument();
});
});
});

Dependency Injection Mocks

// src/services/order.service.ts
interface PaymentGateway {
charge(amount: number, token: string): Promise<{ transactionId: string }>;
}

interface InventoryService {
reserve(items: OrderItem[]): Promise<string>;
release(reservationId: string): Promise<void>;
}

export class OrderService {
constructor(
private readonly paymentGateway: PaymentGateway,
private readonly inventory: InventoryService,
private readonly orderRepo: OrderRepository
) {}

async createOrder(order: CreateOrderDTO): Promise<Order> {
// Reserve inventory
const reservationId = await this.inventory.reserve(order.items);

try {
// Charge payment
const payment = await this.paymentGateway.charge(
order.total,
order.paymentToken
);

// Create order
const newOrder = await this.orderRepo.create({
...order,
transactionId: payment.transactionId,
status: 'confirmed',
});

return newOrder;
} catch (error) {
// Release inventory on failure
await this.inventory.release(reservationId);
throw error;
}
}
}

// tests/order.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OrderService } from '../src/services/order.service';

describe('OrderService', () => {
let orderService: OrderService;
let mockPaymentGateway: PaymentGateway;
let mockInventory: InventoryService;
let mockOrderRepo: OrderRepository;

beforeEach(() => {
mockPaymentGateway = {
charge: vi.fn().mockResolvedValue({ transactionId: 'txn_123' }),
};

mockInventory = {
reserve: vi.fn().mockResolvedValue('res_123'),
release: vi.fn().mockResolvedValue(undefined),
};

mockOrderRepo = {
create: vi.fn().mockImplementation((order) => ({
id: 'order_123',
...order,
})),
};

orderService = new OrderService(
mockPaymentGateway,
mockInventory,
mockOrderRepo
);
});

it('should create order with payment and inventory', async () => {
const orderData = {
items: [{ productId: 'prod_1', quantity: 2 }],
total: 100,
paymentToken: 'tok_123',
};

const order = await orderService.createOrder(orderData);

expect(mockInventory.reserve).toHaveBeenCalledWith(orderData.items);
expect(mockPaymentGateway.charge).toHaveBeenCalledWith(100, 'tok_123');
expect(order.status).toBe('confirmed');
expect(order.transactionId).toBe('txn_123');
});

it('should release inventory on payment failure', async () => {
mockPaymentGateway.charge = vi.fn().mockRejectedValue(
new Error('Payment declined')
);

const orderData = {
items: [{ productId: 'prod_1', quantity: 2 }],
total: 100,
paymentToken: 'tok_123',
};

await expect(orderService.createOrder(orderData)).rejects.toThrow(
'Payment declined'
);

expect(mockInventory.reserve).toHaveBeenCalled();
expect(mockInventory.release).toHaveBeenCalledWith('res_123');
});
});

React Testing Library

// tests/components/LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from '../src/components/LoginForm';

describe('LoginForm', () => {
it('should submit with valid credentials', async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();

render(<LoginForm onSubmit={onSubmit} />);

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: /sign in/i }));

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

it('should show validation errors', async () => {
const user = userEvent.setup();

render(<LoginForm onSubmit={vi.fn()} />);

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

expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});

it('should disable submit while loading', async () => {
const user = userEvent.setup();
const slowSubmit = vi.fn(() => new Promise(r => setTimeout(r, 1000)));

render(<LoginForm onSubmit={slowSubmit} />);

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: /sign in/i }));

expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled();
});
});

Coverage Strategy

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.*',
'**/types/',
],
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
},
});

Usage Examples

Implement TDD for New Feature

Apply testing-strategies skill to write tests first for a shopping cart service

Add Integration Tests

Apply testing-strategies skill to create integration tests for the user registration flow

Mock External Services

Apply testing-strategies skill to set up MSW handlers for third-party API integration tests

Integration Points

  • e2e-testing - Playwright/Cypress patterns
  • error-handling-resilience - Error case testing
  • cicd-pipeline-design - Test automation in CI

Success Output

When successful, this skill MUST output:

✅ SKILL COMPLETE: testing-strategies

Completed:
- [x] Test suite implemented (unit, integration, e2e as applicable)
- [x] TDD cycle followed (red-green-refactor)
- [x] Mock services configured (MSW, dependency injection)
- [x] Code coverage meets thresholds (80%+ statements/functions)
- [x] All tests passing in CI/CD pipeline

Test Results:
- Total tests: X (Unit: Y, Integration: Z, E2E: W)
- Coverage: Statements X%, Branches Y%, Functions Z%, Lines W%
- Test execution time: X.XXs
- Passing: X/X (100%)

Outputs:
- Test files: tests/**/*.test.{ts,tsx,js,jsx,py,rs}
- Coverage reports: coverage/index.html
- MSW handlers: tests/mocks/handlers.ts (if applicable)
- Test configuration: vitest.config.ts, pytest.ini, etc.

Completion Checklist

Before marking this skill as complete, verify:

  • All test suites pass locally (npm test, pytest, cargo test)
  • Code coverage meets minimum thresholds (80%+ per category)
  • Critical paths have integration tests (not just unit tests)
  • Error cases tested comprehensively
  • Async operations tested with proper awaits
  • Mock services configured (MSW for API calls, dependency injection for services)
  • Test isolation verified (no interdependencies between tests)
  • CI/CD integration configured and passing
  • Test documentation updated (README with test commands)
  • No flaky tests (consistent pass/fail across multiple runs)

Failure Indicators

This skill has FAILED if:

  • ❌ Tests failing locally or in CI/CD
  • ❌ Code coverage below 80% thresholds
  • ❌ Missing tests for critical functionality
  • ❌ Flaky tests (intermittent failures)
  • ❌ Test timeouts or hanging tests
  • ❌ Mocks not working (API calls hitting real endpoints)
  • ❌ Test pollution (tests affecting each other)
  • ❌ Syntax errors in test files
  • ❌ Missing test dependencies (packages not installed)
  • ❌ Coverage reports not generated or inaccessible

When NOT to Use

Do NOT use this skill when:

  • Exploratory coding phase (write tests after spike validated)
  • Prototyping UI/UX (visual validation more important)
  • Legacy code without test infrastructure (use legacy-testing-patterns first)
  • Performance benchmarking only needed (use performance-testing skill)
  • Simple scripts with no business logic (testing overhead not justified)
  • Third-party integrations with own test suites (use integration tests only)
  • Static content or configuration files (no logic to test)
  • Emergency hotfixes (add tests after fix deployed)

Use alternatives:

  • Performance testing → performance-testing skill
  • Legacy code → legacy-testing-patterns skill
  • E2E testing → e2e-testing-patterns skill
  • Visual testing → visual-regression-testing skill

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Tests after implementationMisses design feedback from TDDWrite tests first (red-green-refactor)
Testing implementation detailsBrittle tests that break on refactorTest behavior/outcomes, not internal structure
No test isolationTests fail when run in different orderUse beforeEach/afterEach, avoid shared state
Ignoring edge casesProduction bugs in corner casesTest boundaries, null/undefined, empty arrays
Hard-coded test dataTests fail when data changesUse factories, fixtures, property-based testing
Mocking everythingTests don't catch integration issuesBalance mocks with integration tests
Copy-paste test codeHard to maintain, inconsistentExtract test helpers, use parameterized tests
No negative testsOnly testing happy pathTest error conditions, validation failures
Flaky async testsUnreliable CI/CD pipelineProper async/await, increase timeouts if needed
Chasing 100% coverageDiminishing returns, wasted effortFocus on critical paths, 80-90% is sufficient

Principles

This skill embodies:

  • #2 First Principles - TDD starts with understanding WHY test is needed
  • #3 Keep It Simple - AAA pattern (Arrange-Act-Assert) for clarity
  • #4 Separation of Concerns - Unit tests isolated from integration tests
  • #5 Eliminate Ambiguity - Clear test names describe expected behavior
  • #8 No Assumptions - Tests verify behavior explicitly
  • #9 Quality Over Speed - Comprehensive tests prevent production bugs

Full Standard: CODITECT-STANDARD-AUTOMATION.md