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.
Provides comprehensive testing guidance covering test pyramid, TDD, mocking, and CI/CD integration. Use when writing tests, debugging failures, or setting up testing infrastructure.
/plugin marketplace add webdevtodayjason/titanium-plugins/plugin install titanium-toolkit@titanium-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill provides comprehensive guidance for implementing effective testing strategies across your entire application stack.
/\
/ \
/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:
Unit tests are cheap:
Integration tests are moderate:
E2E tests are expensive:
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;
}
}
Design benefits:
Quality benefits:
Workflow benefits:
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',
});
});
✅ DO mock:
// 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);
});
❌ DON'T mock:
// ❌ 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);
});
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);
}
}
Line Coverage: Percentage of code lines executed
Branch Coverage: Percentage of if/else branches tested
Function Coverage: Percentage of functions called
Statement Coverage: Percentage of statements executed
// 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$/"
]
}
}
High priority (aim for 95%+ coverage):
Medium priority (aim for 80%+ coverage):
Low priority (optional coverage):
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);
});
});
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');
});
});
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');
});
});
// 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' } },
],
});
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');
});
});
Test these critical user journeys:
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);
}
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);
});
});
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);
});
afterEach(async () => {
await database.truncate();
jest.clearAllMocks();
jest.useRealTimers();
});
// ❌ BAD
await sleep(1000);
// ✅ GOOD
await waitFor(() => expect(element).toBeInTheDocument());
test('creates user', async () => {
const uniqueEmail = `test-${Date.now()}@example.com`;
const user = await createUser({ email: uniqueEmail });
expect(user.email).toBe(uniqueEmail);
});
// 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
});
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);
});
// 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();
});
# .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
// 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"
}
}
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') }}
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
// 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
});
});
});
Use this skill when:
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.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.