Apply test pyramid principles, coverage targets, and framework-specific patterns. Use when designing test suites, reviewing test coverage, or implementing tests. Covers Jest, Pytest, and common testing frameworks with naming conventions and organization patterns.
/plugin marketplace add rsmdt/the-startup/plugin install team@the-startupThis skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/test-pyramid.mdA development skill that provides comprehensive testing methodology including test pyramid structure, coverage targets, framework-specific patterns, and test organization conventions.
The test pyramid guides test distribution for optimal feedback speed and confidence:
/\
/ \ E2E Tests (5-10%)
/----\ - Critical user journeys
/ \ - Slow, expensive, flaky-prone
/--------\
/ \ Integration Tests (20-30%)
/ Service \ - API contracts, database
/--------------\ - Component interactions
/ \
/ Unit Tests \ Unit Tests (60-70%)
/==================\ - Fast, isolated, deterministic
- Business logic focus
| Test Type | Target % | Execution Time | Scope |
|---|---|---|---|
| Unit | 60-70% | < 100ms each | Single function/class |
| Integration | 20-30% | < 5s each | Service boundaries |
| E2E | 5-10% | < 30s each | Critical user paths |
Tests should verify observable behavior, not internal implementation details.
// CORRECT: Tests behavior
describe('ShoppingCart', () => {
it('calculates total with quantity discounts', () => {
const cart = new ShoppingCart();
cart.add({ sku: 'WIDGET', price: 10, quantity: 5 });
expect(cart.total).toBe(45); // 10% discount for 5+ items
});
});
// INCORRECT: Tests implementation
describe('ShoppingCart', () => {
it('calls _applyDiscount method', () => {
const cart = new ShoppingCart();
const spy = jest.spyOn(cart, '_applyDiscount');
cart.add({ sku: 'WIDGET', price: 10, quantity: 5 });
expect(spy).toHaveBeenCalledWith(0.1);
});
});
Every test follows the AAA pattern for clarity and consistency.
def test_user_registration_sends_welcome_email():
# Arrange
email_service = MockEmailService()
user_service = UserService(email_service=email_service)
registration_data = {"email": "new@user.com", "name": "New User"}
# Act
user_service.register(registration_data)
# Assert
assert email_service.sent_emails == [
{"to": "new@user.com", "template": "welcome"}
]
Each test verifies one specific behavior. Multiple assertions are acceptable when verifying a single logical outcome.
// CORRECT: One behavior, multiple related assertions
it('creates order with correct initial state', () => {
const order = createOrder(items);
expect(order.status).toBe('pending');
expect(order.items).toHaveLength(items.length);
expect(order.createdAt).toBeInstanceOf(Date);
});
// INCORRECT: Multiple unrelated behaviors
it('creates and processes order', () => {
const order = createOrder(items);
expect(order.status).toBe('pending');
processPayment(order);
expect(order.status).toBe('paid'); // Different behavior
});
Test names describe the scenario and expected outcome.
// Format: [unit]_[scenario]_[expected outcome]
// or: [action]_[condition]_[result]
// CORRECT
it('calculateTotal returns zero for empty cart')
it('validateEmail rejects addresses without @ symbol')
it('UserService throws NotFoundError when user does not exist')
// INCORRECT
it('test calculateTotal')
it('validateEmail works')
it('error handling')
Tests must be independent and not share mutable state.
# CORRECT: Fresh fixture per test
class TestOrderService:
def setup_method(self):
self.db = InMemoryDatabase()
self.service = OrderService(self.db)
def test_create_order(self):
order = self.service.create(items=[...])
assert order.id is not None
def test_list_orders_empty(self):
orders = self.service.list()
assert orders == []
# INCORRECT: Shared state between tests
db = Database() # Shared!
def test_create_order():
order = OrderService(db).create(items=[...])
def test_list_orders_empty():
orders = OrderService(db).list() # May see order from previous test!
| Code Type | Statement | Branch | Target |
|---|---|---|---|
| Business Logic | 90% | 85% | High |
| API Controllers | 80% | 75% | Medium |
| Utility Functions | 95% | 90% | High |
| UI Components | 70% | 65% | Medium |
| Generated Code | N/A | N/A | Skip |
Coverage percentage alone is insufficient. Prioritize:
// File structure
src/
services/
UserService.ts
UserService.test.ts // Co-located
// Test setup
describe('UserService', () => {
let userService: UserService;
let mockRepo: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepo = {
findById: jest.fn(),
save: jest.fn(),
};
userService = new UserService(mockRepo);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getUser', () => {
it('returns user when found', async () => {
const expected = { id: '1', name: 'Alice' };
mockRepo.findById.mockResolvedValue(expected);
const result = await userService.getUser('1');
expect(result).toEqual(expected);
expect(mockRepo.findById).toHaveBeenCalledWith('1');
});
it('throws NotFoundError when user does not exist', async () => {
mockRepo.findById.mockResolvedValue(null);
await expect(userService.getUser('999'))
.rejects.toThrow(NotFoundError);
});
});
});
# File structure
src/
services/
user_service.py
tests/
services/
test_user_service.py # Mirror structure
# conftest.py - Shared fixtures
import pytest
from unittest.mock import Mock
@pytest.fixture
def mock_user_repo():
return Mock(spec=UserRepository)
@pytest.fixture
def user_service(mock_user_repo):
return UserService(repository=mock_user_repo)
# test_user_service.py
class TestUserService:
def test_get_user_returns_user_when_found(
self, user_service, mock_user_repo
):
expected = User(id="1", name="Alice")
mock_user_repo.find_by_id.return_value = expected
result = user_service.get_user("1")
assert result == expected
mock_user_repo.find_by_id.assert_called_once_with("1")
def test_get_user_raises_not_found_when_missing(
self, user_service, mock_user_repo
):
mock_user_repo.find_by_id.return_value = None
with pytest.raises(NotFoundError):
user_service.get_user("999")
@pytest.mark.parametrize("invalid_email", [
"",
"no-at-sign",
"@no-local-part.com",
"no-domain@",
])
def test_validate_email_rejects_invalid_formats(
self, user_service, invalid_email
):
assert not user_service.validate_email(invalid_email)
// Component test
import { render, screen, userEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('calls onSubmit with credentials when form is submitted', async () => {
const onSubmit = jest.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Password'), 'secret123');
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'secret123',
});
});
it('displays validation error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={jest.fn()} />);
await user.type(screen.getByLabelText('Email'), 'invalid');
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(screen.getByText('Please enter a valid email')).toBeInTheDocument();
});
});
| Framework | Test File Pattern | Example |
|---|---|---|
| Jest | *.test.ts | UserService.test.ts |
| Pytest | test_*.py | test_user_service.py |
| Go | *_test.go | user_service_test.go |
| JUnit | *Test.java | UserServiceTest.java |
Co-located tests (preferred for unit tests):
src/
services/
UserService.ts
UserService.test.ts
utils/
validators.ts
validators.test.ts
Separate test directory (for integration/E2E):
src/
services/
UserService.ts
tests/
unit/
services/
UserService.test.ts
integration/
api/
users.test.ts
e2e/
user-registration.spec.ts
// WRONG: Brittle test that breaks on refactoring
it('stores user in _users array', () => {
const service = new UserService();
service.addUser(user);
expect(service._users).toContain(user);
});
# WRONG: Tests interfere with each other
users = []
def test_add_user():
users.append(User())
def test_user_count():
assert len(users) == 0 # Fails if test_add_user runs first
// WRONG: Mocking the system under test
it('processes payment', () => {
const processor = new PaymentProcessor();
jest.spyOn(processor, 'validate').mockReturnValue(true);
jest.spyOn(processor, 'charge').mockResolvedValue({ success: true });
// What are we even testing?
const result = processor.process(payment);
});
# WRONG: Copy-paste tests with minor variations
def test_validate_email_empty():
assert not validate_email("")
def test_validate_email_no_at():
assert not validate_email("invalid")
def test_validate_email_no_domain():
assert not validate_email("user@")
# CORRECT: Parameterized test
@pytest.mark.parametrize("invalid_email", ["", "invalid", "user@"])
def test_validate_email_rejects_invalid_formats(invalid_email):
assert not validate_email(invalid_email)
examples/test-pyramid.md - Detailed test pyramid implementation guide with framework examplesThis 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.