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
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
TDD, unit testing, integration testing, mocking, and test automation patterns for high-quality codebases.
Core Capabilities
- TDD Workflow - Red-green-refactor cycle
- Unit Testing - Isolated component tests
- Integration Testing - System interaction tests
- Mocking - Test doubles, MSW, dependency injection
- 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-patternsfirst) - Performance benchmarking only needed (use
performance-testingskill) - 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-testingskill - Legacy code →
legacy-testing-patternsskill - E2E testing →
e2e-testing-patternsskill - Visual testing →
visual-regression-testingskill
Anti-Patterns (Avoid)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Tests after implementation | Misses design feedback from TDD | Write tests first (red-green-refactor) |
| Testing implementation details | Brittle tests that break on refactor | Test behavior/outcomes, not internal structure |
| No test isolation | Tests fail when run in different order | Use beforeEach/afterEach, avoid shared state |
| Ignoring edge cases | Production bugs in corner cases | Test boundaries, null/undefined, empty arrays |
| Hard-coded test data | Tests fail when data changes | Use factories, fixtures, property-based testing |
| Mocking everything | Tests don't catch integration issues | Balance mocks with integration tests |
| Copy-paste test code | Hard to maintain, inconsistent | Extract test helpers, use parameterized tests |
| No negative tests | Only testing happy path | Test error conditions, validation failures |
| Flaky async tests | Unreliable CI/CD pipeline | Proper async/await, increase timeouts if needed |
| Chasing 100% coverage | Diminishing returns, wasted effort | Focus 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