Comprehensive testing strategy covering test pyramids, framework selection, coverage standards, test organization, mocking patterns, and CI/CD integration. Activate when planning testing approaches, setting quality gates, or establishing test standards.
Provides comprehensive testing strategy guidance covering test pyramids, framework selection, coverage standards, and CI/CD integration. Activates when planning testing approaches, establishing quality gates, or setting up test standards for projects.
/plugin marketplace add squirrelsoft-dev/agency/plugin install agency@squirrelsoft-dev-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/test-patterns.mdreferences/coverage-standards.mdMaster comprehensive testing strategies that ensure code quality, catch bugs early, and enable confident deployments. This skill covers test pyramids, framework selection, coverage standards, and continuous testing practices.
Primary Goals:
Not Goals:
Test Behavior, Not Implementation:
// ❌ Bad: Testing implementation
test('calls useState with initial value', () => {
const spy = jest.spyOn(React, 'useState');
render(<Counter />);
expect(spy).toHaveBeenCalledWith(0);
});
// ✅ Good: Testing behavior
test('displays initial count of 0', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
Write Tests Users Would Write:
// ✅ Test from user perspective
test('user can add item to cart', () => {
render(<ProductPage />);
// Find product
const addButton = screen.getByRole('button', { name: /add to cart/i });
// Click button
userEvent.click(addButton);
// Verify result
expect(screen.getByText('1 item in cart')).toBeInTheDocument();
});
/\
/ \
/ E2E \ 10% - Slow, expensive, brittle
/------\
/ Integ. \ 20% - Medium speed, medium cost
/----------\
/ Unit \ 70% - Fast, cheap, stable
/--------------\
Distribution Guidelines:
What to Test:
Characteristics:
Example:
// ✅ Good unit test
describe('formatCurrency', () => {
it('should format USD correctly', () => {
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
});
it('should handle zero', () => {
expect(formatCurrency(0, 'USD')).toBe('$0.00');
});
it('should handle negative amounts', () => {
expect(formatCurrency(-100, 'USD')).toBe('-$100.00');
});
});
What to Test:
Characteristics:
Example:
// ✅ Integration test
describe('POST /api/users', () => {
beforeEach(async () => {
await setupTestDatabase();
});
afterEach(async () => {
await cleanupTestDatabase();
});
it('should create user and send welcome email', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test User' });
expect(response.status).toBe(201);
expect(response.body.user.email).toBe('test@example.com');
// Verify database
const user = await db.users.findByEmail('test@example.com');
expect(user).toBeDefined();
// Verify email sent
expect(emailService.send).toHaveBeenCalledWith({
to: 'test@example.com',
template: 'welcome'
});
});
});
What to Test:
Characteristics:
Example:
// ✅ E2E test
test('user can complete checkout process', async ({ page }) => {
// Login
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Add to cart
await page.goto('/products/123');
await page.click('button:has-text("Add to Cart")');
// Checkout
await page.goto('/cart');
await page.click('button:has-text("Checkout")');
await page.fill('[name="cardNumber"]', '4242424242424242');
await page.fill('[name="expiry"]', '12/25');
await page.fill('[name="cvc"]', '123');
await page.click('button:has-text("Complete Purchase")');
// Verify success
await expect(page.locator('text=Order Confirmed')).toBeVisible();
});
See Test Pyramid Details for comprehensive guidelines.
Jest (Most Popular):
// Pros: All-in-one, great React support, snapshot testing
// Cons: Can be slow for large codebases
// Best for: React apps, Node.js backends
describe('UserService', () => {
it('should fetch user by id', async () => {
const user = await UserService.getById('123');
expect(user).toMatchObject({
id: '123',
name: expect.any(String)
});
});
});
Vitest (Modern Alternative):
// Pros: Very fast, Vite integration, Jest-compatible API
// Cons: Newer, smaller ecosystem
// Best for: Vite projects, new projects wanting speed
import { describe, it, expect } from 'vitest';
describe('UserService', () => {
it('should fetch user by id', async () => {
const user = await UserService.getById('123');
expect(user).toMatchObject({
id: '123',
name: expect.any(String)
});
});
});
Supertest (API Testing):
// Best for: Testing REST APIs
import request from 'supertest';
import app from './app';
describe('API Integration', () => {
it('GET /api/users should return users', async () => {
const response = await request(app)
.get('/api/users')
.expect(200)
.expect('Content-Type', /json/);
expect(response.body).toHaveLength(10);
});
});
Playwright (Recommended):
// Pros: Fast, reliable, multi-browser, great dev tools
// Cons: Newer
// Best for: Modern web apps, cross-browser testing
import { test, expect } from '@playwright/test';
test('user can login', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await expect(page.locator('text=Dashboard')).toBeVisible();
});
Cypress (Popular Alternative):
// Pros: Great DX, time-travel debugging, easy to learn
// Cons: Runs in browser (limitations), slower than Playwright
// Best for: Developers new to E2E testing
describe('Login', () => {
it('should allow user to login', () => {
cy.visit('/login');
cy.get('[name="email"]').type('user@example.com');
cy.get('[name="password"]').type('password');
cy.get('button[type="submit"]').click();
cy.contains('Dashboard').should('be.visible');
});
});
Minimum Targets:
{
"jest": {
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
},
"critical": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
}
}
}
By Code Type:
Good Coverage ≠ Good Tests:
// ❌ 100% coverage but useless test
test('user service exists', () => {
const service = new UserService();
expect(service).toBeDefined();
service.getUser('123'); // Called but not verified!
service.createUser({}); // Called but not verified!
});
// ✅ Lower coverage but meaningful tests
test('getUser returns user when found', async () => {
const user = await UserService.getUser('123');
expect(user).toMatchObject({
id: '123',
email: 'test@example.com'
});
});
test('getUser throws when user not found', async () => {
await expect(UserService.getUser('999'))
.rejects.toThrow(NotFoundError);
});
What Good Coverage Includes:
See Coverage Standards for detailed requirements.
Co-located Tests (Recommended):
src/
components/
Button/
Button.tsx
Button.test.tsx
Button.stories.tsx
services/
UserService.ts
UserService.test.ts
utils/
formatters.ts
formatters.test.ts
Separate Test Directory:
src/
components/
Button.tsx
services/
UserService.ts
__tests__/
components/
Button.test.tsx
services/
UserService.test.ts
Test Files:
Component.test.tsx (Jest/Vitest)Component.spec.tsx (Angular convention)Component.integration.test.ts (Integration tests)Component.e2e.test.ts (E2E tests)Test Names:
// ✅ Good: Descriptive, readable
describe('UserAuthentication', () => {
describe('login', () => {
it('should return user when credentials are valid', () => {});
it('should throw error when password is incorrect', () => {});
it('should lock account after 5 failed attempts', () => {});
});
describe('logout', () => {
it('should clear session token', () => {});
it('should redirect to login page', () => {});
});
});
// ❌ Bad: Vague, unclear
describe('Auth', () => {
it('works', () => {});
it('fails sometimes', () => {});
});
By Feature:
describe('Shopping Cart', () => {
describe('adding items', () => {
it('should add item to empty cart', () => {});
it('should increase quantity if item exists', () => {});
it('should enforce maximum quantity', () => {});
});
describe('removing items', () => {
it('should remove item from cart', () => {});
it('should handle removing non-existent item', () => {});
});
describe('calculating total', () => {
it('should sum all item prices', () => {});
it('should apply discounts', () => {});
it('should include tax', () => {});
});
});
Mock External Dependencies:
Don't Mock Internal Logic:
API Mocking:
// ✅ Mock fetch with jest
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ id: '123', name: 'Test User' })
})
);
test('fetches user data', async () => {
const user = await fetchUser('123');
expect(user.name).toBe('Test User');
expect(fetch).toHaveBeenCalledWith('/api/users/123');
});
Database Mocking:
// ✅ Use test database
import { setupTestDB, cleanupTestDB } from './test-helpers';
beforeAll(async () => {
await setupTestDB();
});
afterAll(async () => {
await cleanupTestDB();
});
test('creates user in database', async () => {
const user = await UserService.create({
email: 'test@example.com',
name: 'Test'
});
const found = await UserService.findById(user.id);
expect(found.email).toBe('test@example.com');
});
Service Mocking:
// ✅ Mock service dependencies
jest.mock('./EmailService');
import EmailService from './EmailService';
test('sends welcome email when user registers', async () => {
await UserService.register({ email: 'test@example.com' });
expect(EmailService.send).toHaveBeenCalledWith({
to: 'test@example.com',
template: 'welcome'
});
});
Time Mocking:
// ✅ Mock dates for consistent tests
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01'));
});
afterAll(() => {
jest.useRealTimers();
});
test('calculates days until expiry', () => {
const expiry = new Date('2024-01-10');
const days = calculateDaysUntil(expiry);
expect(days).toBe(9);
});
See Test Patterns for common mocking patterns.
// ✅ Return promise
test('fetches user', () => {
return fetchUser('123').then(user => {
expect(user.id).toBe('123');
});
});
// ✅ Use async/await (preferred)
test('fetches user', async () => {
const user = await fetchUser('123');
expect(user.id).toBe('123');
});
// ✅ Test rejections
test('throws when user not found', async () => {
await expect(fetchUser('999')).rejects.toThrow(NotFoundError);
});
// ✅ Use done callback
test('calls callback with result', (done) => {
fetchUser('123', (error, user) => {
expect(error).toBeNull();
expect(user.id).toBe('123');
done();
});
});
// ✅ Fast-forward time
jest.useFakeTimers();
test('debounces input', () => {
const callback = jest.fn();
const debounced = debounce(callback, 1000);
debounced('a');
debounced('b');
debounced('c');
expect(callback).not.toHaveBeenCalled();
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('c');
});
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
- name: Check coverage threshold
run: npm run test:coverage-check
Pre-merge Requirements:
Configuration:
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"test:coverage-check": "jest --coverage --coverageReporters=text-summary --passWithNoTests",
"test:ci": "jest --ci --coverage --maxWorkers=2"
}
}
Arrange, Act, Assert:
test('adds item to cart', () => {
// Arrange - Set up test data
const cart = new ShoppingCart();
const item = { id: '123', name: 'Product', price: 29.99 };
// Act - Perform the action
cart.addItem(item);
// Assert - Verify the result
expect(cart.items).toHaveLength(1);
expect(cart.total).toBe(29.99);
});
// ✅ Clean state between tests
describe('ShoppingCart', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart(); // Fresh instance
});
test('adds item', () => {
cart.addItem(item1);
expect(cart.items).toHaveLength(1);
});
test('removes item', () => {
cart.addItem(item1);
cart.removeItem(item1.id);
expect(cart.items).toHaveLength(0);
});
});
// ❌ Bad: Multiple unrelated assertions
test('user service', async () => {
const user = await UserService.create(data);
expect(user.id).toBeDefined();
const found = await UserService.findById(user.id);
expect(found).toBeDefined();
await UserService.delete(user.id);
const deleted = await UserService.findById(user.id);
expect(deleted).toBeNull();
});
// ✅ Good: Focused tests
test('creates user with generated id', async () => {
const user = await UserService.create(data);
expect(user.id).toBeDefined();
});
test('finds created user by id', async () => {
const user = await UserService.create(data);
const found = await UserService.findById(user.id);
expect(found).toMatchObject(data);
});
test('deletes user', async () => {
const user = await UserService.create(data);
await UserService.delete(user.id);
const deleted = await UserService.findById(user.id);
expect(deleted).toBeNull();
});
// ❌ Bad: Magic values
test('validates email', () => {
expect(validateEmail('a@b.c')).toBe(true);
});
// ✅ Good: Clear intent
test('validates email', () => {
const validEmail = 'user@example.com';
expect(validateEmail(validEmail)).toBe(true);
});
test('rejects invalid email format', () => {
const invalidEmail = 'not-an-email';
expect(validateEmail(invalidEmail)).toBe(false);
});
// ❌ Bad: Tests internal state
test('counter increments state', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0); // Implementation detail
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
// ✅ Good: Tests behavior
test('displays incremented count', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(screen.getByText('Increment'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
Causes & Solutions:
Optimization:
Test Types:
Coverage Targets:
Frameworks:
CI/CD:
code-review-standards - What to look for in test reviewsgithub-workflow-best-practices - CI/CD integrationagency-workflow-patterns - Using test automation agentsRemember: Tests are documentation and safety nets. Write tests that give confidence and catch real bugs, not tests for coverage numbers.
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.