write-tests
SKILL.md
Write Tests Skill
When writing tests, follow this structured process. Good tests verify behavior, catch regressions, and serve as living documentation.
1. Discovery — Understand What to Test
Before writing any tests, analyze the code:
Detect Testing Setup
# Detect framework and config
cat package.json 2>/dev/null | grep -E "jest|vitest|mocha|ava|playwright|cypress|testing-library"
cat jest.config.* vitest.config.* 2>/dev/null | head -30
cat pyproject.toml 2>/dev/null | grep -A 20 "\[tool.pytest"
cat pytest.ini conftest.py 2>/dev/null | head -20
cat .rspec spec/spec_helper.rb 2>/dev/null | head -20
cat phpunit.xml 2>/dev/null | head -20
# Find existing test examples to match patterns
find . -name "*.test.*" -o -name "*.spec.*" -o -name "test_*" -o -name "*_test.*" | head -10
# Read an existing test file to understand conventions
# (pick the most recent or most relevant one)
Analyze the Code Under Test
# Read the file to understand its exports and dependencies
cat [target-file]
# Find what it imports (these might need mocking)
grep -E "^import|^from|require\(" [target-file]
# Find what exports it provides (these are what we test)
grep -E "^export|module\.exports" [target-file]
# Find related types/interfaces
grep -E "interface |type |class " [target-file]
Identify Test Cases
For each function/method/class, identify:
- Happy path — normal successful operation
- Edge cases — boundary values, empty inputs, limits
- Error cases — invalid inputs, failures, exceptions
- Integration points — database, API, file system calls
- State transitions — before/after state changes
- Concurrency — race conditions, parallel execution
- Security — malicious inputs, injection attempts
2. Test Structure Principles
Arrange-Act-Assert (AAA)
// Arrange — set up the test data and conditions
// Act — execute the code under test
// Assert — verify the results
Test Naming
Use descriptive names that explain the scenario:
// Pattern: [unit] [scenario] [expected result]
// ✅ GOOD
"createUser returns user with generated UUID when valid email provided"
"createUser throws ValidationError when email is empty string"
"createUser hashes password with bcrypt before saving"
// 🔴 BAD
"createUser works"
"test 1"
"should work correctly"
Test Organization
describe('[Module/Class/Function]', () => {
describe('[method or scenario group]', () => {
it('[specific behavior]', () => { ... });
});
});
What Makes a Good Test
- Independent — no test depends on another test's state
- Deterministic — same result every time (no random, no timing)
- Fast — unit tests < 100ms each
- Readable — a test is documentation; anyone should understand it
- Focused — one assertion concept per test (can have multiple expect statements)
- Realistic — test data resembles real data, not
"test"and123
3. Stack-Specific Test Generation
TypeScript / JavaScript — Jest
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
import { OrderService } from '../services/orderService';
import { OrderRepository } from '../db/repositories/orderRepository';
import { PaymentService } from '../lib/paymentService';
import { NotificationService } from '../lib/notificationService';
import type { CreateOrderInput, Order } from '../models/order';
// Mock dependencies
jest.mock('../db/repositories/orderRepository');
jest.mock('../lib/paymentService');
jest.mock('../lib/notificationService');
describe('OrderService', () => {
let orderService: OrderService;
let mockOrderRepo: jest.Mocked<OrderRepository>;
let mockPaymentService: jest.Mocked<PaymentService>;
let mockNotificationService: jest.Mocked<NotificationService>;
// --- Setup & Teardown ---
beforeEach(() => {
mockOrderRepo = new OrderRepository() as jest.Mocked<OrderRepository>;
mockPaymentService = new PaymentService() as jest.Mocked<PaymentService>;
mockNotificationService = new NotificationService() as jest.Mocked<NotificationService>;
orderService = new OrderService(mockOrderRepo, mockPaymentService, mockNotificationService);
});
afterEach(() => {
jest.restoreAllMocks();
});
// --- Test Data Factories ---
const createValidOrderInput = (overrides?: Partial<CreateOrderInput>): CreateOrderInput => ({
userId: 'user-abc-123',
items: [
{ productId: 'prod-001', quantity: 2, priceInCents: 2999 },
{ productId: 'prod-002', quantity: 1, priceInCents: 4999 },
],
shippingAddress: {
street: '123 Main St',
city: 'Springfield',
state: 'IL',
zip: '62701',
country: 'US',
},
...overrides,
});
const createMockOrder = (overrides?: Partial<Order>): Order => ({
id: 'order-xyz-789',
userId: 'user-abc-123',
status: 'pending',
totalInCents: 10997,
items: [],
createdAt: new Date('2026-01-15T10:00:00Z'),
updatedAt: new Date('2026-01-15T10:00:00Z'),
...overrides,
});
// --- Tests ---
describe('createOrder', () => {
// Happy Path
describe('when given valid input', () => {
it('creates an order with correct total calculated from items', async () => {
const input = createValidOrderInput();
mockOrderRepo.create.mockResolvedValue(createMockOrder({ totalInCents: 10997 }));
mockPaymentService.authorize.mockResolvedValue({ transactionId: 'txn-001' });
const result = await orderService.createOrder(input);
expect(mockOrderRepo.create).toHaveBeenCalledWith(
expect.objectContaining({
userId: 'user-abc-123',
totalInCents: 10997, // (2999 * 2) + (4999 * 1)
})
);
expect(result.totalInCents).toBe(10997);
});
it('authorizes payment before saving the order', async () => {
const input = createValidOrderInput();
mockOrderRepo.create.mockResolvedValue(createMockOrder());
mockPaymentService.authorize.mockResolvedValue({ transactionId: 'txn-001' });
await orderService.createOrder(input);
// Verify payment authorization was called before order creation
const paymentCallOrder = mockPaymentService.authorize.mock.invocationCallOrder[0];
const repoCallOrder = mockOrderRepo.create.mock.invocationCallOrder[0];
expect(paymentCallOrder).toBeLessThan(repoCallOrder);
});
it('sends order confirmation notification', async () => {
const input = createValidOrderInput();
mockOrderRepo.create.mockResolvedValue(createMockOrder());
mockPaymentService.authorize.mockResolvedValue({ transactionId: 'txn-001' });
await orderService.createOrder(input);
expect(mockNotificationService.sendOrderConfirmation).toHaveBeenCalledWith(
expect.objectContaining({
userId: 'user-abc-123',
orderId: 'order-xyz-789',
})
);
});
});
// Edge Cases
describe('edge cases', () => {
it('handles single item order', async () => {
const input = createValidOrderInput({
items: [{ productId: 'prod-001', quantity: 1, priceInCents: 999 }],
});
mockOrderRepo.create.mockResolvedValue(createMockOrder({ totalInCents: 999 }));
mockPaymentService.authorize.mockResolvedValue({ transactionId: 'txn-001' });
const result = await orderService.createOrder(input);
expect(result.totalInCents).toBe(999);
});
it('handles maximum quantity per item', async () => {
const input = createValidOrderInput({
items: [{ productId: 'prod-001', quantity: 99, priceInCents: 100 }],
});
mockOrderRepo.create.mockResolvedValue(createMockOrder({ totalInCents: 9900 }));
mockPaymentService.authorize.mockResolvedValue({ transactionId: 'txn-001' });
const result = await orderService.createOrder(input);
expect(result.totalInCents).toBe(9900);
});
it('applies free shipping for orders over 10000 cents ($100)', async () => {
const input = createValidOrderInput({
items: [{ productId: 'prod-001', quantity: 1, priceInCents: 15000 }],
});
mockOrderRepo.create.mockResolvedValue(createMockOrder({ totalInCents: 15000 }));
mockPaymentService.authorize.mockResolvedValue({ transactionId: 'txn-001' });
const result = await orderService.createOrder(input);
expect(mockOrderRepo.create).toHaveBeenCalledWith(
expect.objectContaining({ shippingCostInCents: 0 })
);
});
});
// Error Cases
describe('error handling', () => {
it('throws ValidationError when items array is empty', async () => {
const input = createValidOrderInput({ items: [] });
await expect(orderService.createOrder(input)).rejects.toThrow('Order must contain at least one item');
});
it('throws ValidationError when quantity is zero or negative', async () => {
const input = createValidOrderInput({
items: [{ productId: 'prod-001', quantity: 0, priceInCents: 999 }],
});
await expect(orderService.createOrder(input)).rejects.toThrow('Quantity must be at least 1');
});
it('throws ValidationError when price is negative', async () => {
const input = createValidOrderInput({
items: [{ productId: 'prod-001', quantity: 1, priceInCents: -100 }],
});
await expect(orderService.createOrder(input)).rejects.toThrow('Price must be a positive number');
});
it('does not save order when payment authorization fails', async () => {
const input = createValidOrderInput();
mockPaymentService.authorize.mockRejectedValue(new Error('Card declined'));
await expect(orderService.createOrder(input)).rejects.toThrow('Card declined');
expect(mockOrderRepo.create).not.toHaveBeenCalled();
});
it('rolls back payment when order save fails', async () => {
const input = createValidOrderInput();
mockPaymentService.authorize.mockResolvedValue({ transactionId: 'txn-001' });
mockOrderRepo.create.mockRejectedValue(new Error('Database connection lost'));
await expect(orderService.createOrder(input)).rejects.toThrow('Database connection lost');
expect(mockPaymentService.refund).toHaveBeenCalledWith('txn-001');
});
it('does not send notification when order creation fails', async () => {
const input = createValidOrderInput();
mockPaymentService.authorize.mockRejectedValue(new Error('Payment failed'));
await expect(orderService.createOrder(input)).rejects.toThrow();
expect(mockNotificationService.sendOrderConfirmation).not.toHaveBeenCalled();
});
it('still completes order when notification fails (non-critical)', async () => {
const input = createValidOrderInput();
mockOrderRepo.create.mockResolvedValue(createMockOrder());
mockPaymentService.authorize.mockResolvedValue({ transactionId: 'txn-001' });
mockNotificationService.sendOrderConfirmation.mockRejectedValue(new Error('Email service down'));
// Should not throw — notification failure is non-critical
const result = await orderService.createOrder(input);
expect(result.id).toBe('order-xyz-789');
});
});
});
describe('cancelOrder', () => {
it('updates order status to cancelled', async () => {
mockOrderRepo.findById.mockResolvedValue(createMockOrder({ status: 'pending' }));
mockOrderRepo.update.mockResolvedValue(createMockOrder({ status: 'cancelled' }));
const result = await orderService.cancelOrder('order-xyz-789', 'user-abc-123');
expect(result.status).toBe('cancelled');
});
it('refunds payment when cancelling a paid order', async () => {
mockOrderRepo.findById.mockResolvedValue(
createMockOrder({ status: 'paid', paymentTransactionId: 'txn-001' })
);
mockOrderRepo.update.mockResolvedValue(createMockOrder({ status: 'cancelled' }));
await orderService.cancelOrder('order-xyz-789', 'user-abc-123');
expect(mockPaymentService.refund).toHaveBeenCalledWith('txn-001');
});
it('throws ForbiddenError when user does not own the order', async () => {
mockOrderRepo.findById.mockResolvedValue(createMockOrder({ userId: 'different-user' }));
await expect(
orderService.cancelOrder('order-xyz-789', 'user-abc-123')
).rejects.toThrow('You do not have permission to cancel this order');
});
it('throws NotFoundError when order does not exist', async () => {
mockOrderRepo.findById.mockResolvedValue(null);
await expect(
orderService.cancelOrder('nonexistent-id', 'user-abc-123')
).rejects.toThrow('Order not found');
});
it('throws ConflictError when order is already shipped', async () => {
mockOrderRepo.findById.mockResolvedValue(createMockOrder({ status: 'shipped' }));
await expect(
orderService.cancelOrder('order-xyz-789', 'user-abc-123')
).rejects.toThrow('Cannot cancel an order that has already been shipped');
});
it('throws ConflictError when order is already cancelled', async () => {
mockOrderRepo.findById.mockResolvedValue(createMockOrder({ status: 'cancelled' }));
await expect(
orderService.cancelOrder('order-xyz-789', 'user-abc-123')
).rejects.toThrow('Order is already cancelled');
});
});
});
TypeScript / JavaScript — Vitest
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { calculateShipping } from '../services/shipping';
import type { Order, Address } from '../models/order';
describe('calculateShipping', () => {
const createOrder = (overrides?: Partial<Order>): Order => ({
id: 'order-001',
totalWeightGrams: 500,
totalInCents: 5000,
items: [{ productId: 'p1', quantity: 1, priceInCents: 5000 }],
...overrides,
});
const createAddress = (overrides?: Partial<Address>): Address => ({
country: 'US',
state: 'CA',
zip: '90210',
...overrides,
});
it('calculates domestic standard shipping based on weight', () => {
const order = createOrder({ totalWeightGrams: 1000 });
const address = createAddress();
const cost = calculateShipping(order, address, 'standard');
expect(cost).toBe(599); // base rate for 1kg domestic
});
it('returns zero for orders over $100 (free shipping)', () => {
const order = createOrder({ totalInCents: 15000 });
const address = createAddress();
const cost = calculateShipping(order, address, 'standard');
expect(cost).toBe(0);
});
it('charges higher rate for express shipping', () => {
const order = createOrder({ totalWeightGrams: 1000 });
const address = createAddress();
const standard = calculateShipping(order, address, 'standard');
const express = calculateShipping(order, address, 'express');
expect(express).toBeGreaterThan(standard);
});
it('charges international rate for non-US addresses', () => {
const order = createOrder({ totalWeightGrams: 1000 });
const domestic = createAddress({ country: 'US' });
const international = createAddress({ country: 'DE' });
const domesticCost = calculateShipping(order, domestic, 'standard');
const internationalCost = calculateShipping(order, international, 'standard');
expect(internationalCost).toBeGreaterThan(domesticCost);
});
it('throws InvalidAddressError for unsupported countries', () => {
const order = createOrder();
const address = createAddress({ country: 'XX' });
expect(() => calculateShipping(order, address, 'standard')).toThrow('Unsupported shipping destination');
});
it('handles zero-weight orders (digital products)', () => {
const order = createOrder({ totalWeightGrams: 0 });
const address = createAddress();
const cost = calculateShipping(order, address, 'standard');
expect(cost).toBe(0);
});
});
React Component — Testing Library
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LoginForm } from '../components/LoginForm';
// Mock the auth service
const mockLogin = vi.fn();
vi.mock('../services/auth', () => ({
useAuth: () => ({
login: mockLogin,
isLoading: false,
error: null,
}),
}));
describe('LoginForm', () => {
const user = userEvent.setup();
beforeEach(() => {
vi.clearAllMocks();
});
// --- Rendering ---
it('renders email and password fields', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
});
it('renders forgot password link', () => {
render(<LoginForm />);
expect(screen.getByRole('link', { name: /forgot password/i })).toHaveAttribute(
'href',
'/forgot-password'
);
});
// --- Form Interaction ---
it('allows typing in email and password fields', async () => {
render(<LoginForm />);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
await user.type(emailInput, 'alice@example.com');
await user.type(passwordInput, 'securePass123');
expect(emailInput).toHaveValue('alice@example.com');
expect(passwordInput).toHaveValue('securePass123');
});
it('submits form with email and password', async () => {
mockLogin.mockResolvedValue({ success: true });
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'securePass123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(mockLogin).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'securePass123',
});
});
it('submits form on Enter key press', async () => {
mockLogin.mockResolvedValue({ success: true });
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'securePass123{enter}');
expect(mockLogin).toHaveBeenCalled();
});
// --- Validation ---
it('shows error when email is empty on submit', async () => {
render(<LoginForm />);
await user.type(screen.getByLabelText(/password/i), 'securePass123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(mockLogin).not.toHaveBeenCalled();
});
it('shows error when password is empty on submit', async () => {
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
expect(mockLogin).not.toHaveBeenCalled();
});
it('shows error for invalid email format', async () => {
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'not-an-email');
await user.type(screen.getByLabelText(/password/i), 'securePass123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/enter a valid email/i)).toBeInTheDocument();
expect(mockLogin).not.toHaveBeenCalled();
});
// --- Loading State ---
it('disables submit button while loading', async () => {
mockLogin.mockImplementation(() => new Promise(() => {})); // never resolves
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'securePass123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled();
});
});
it('shows loading spinner during submission', async () => {
mockLogin.mockImplementation(() => new Promise(() => {}));
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'securePass123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
});
// --- Error Handling ---
it('displays server error message on failed login', async () => {
mockLogin.mockRejectedValue(new Error('Invalid email or password'));
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'wrongPassword');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/invalid email or password/i);
});
});
it('displays generic error on network failure', async () => {
mockLogin.mockRejectedValue(new Error('Network error'));
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'securePass123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/something went wrong/i);
});
});
it('clears error when user starts typing again', async () => {
mockLogin.mockRejectedValue(new Error('Invalid email or password'));
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'wrong');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
// Start typing again — error should clear
await user.type(screen.getByLabelText(/password/i), 'x');
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
// --- Accessibility ---
it('focuses email field on mount', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/email/i)).toHaveFocus();
});
it('moves focus to first error field on validation failure', async () => {
render(<LoginForm />);
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByLabelText(/email/i)).toHaveFocus();
});
it('password field has type="password"', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/password/i)).toHaveAttribute('type', 'password');
});
});
Python — Pytest
import pytest
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch
from decimal import Decimal
from app.services.order_service import OrderService
from app.models.order import Order, OrderStatus, CreateOrderInput, OrderItem
from app.exceptions import ValidationError, NotFoundError, ForbiddenError, ConflictError
# --- Fixtures ---
@pytest.fixture
def mock_order_repo():
repo = AsyncMock()
return repo
@pytest.fixture
def mock_payment_service():
service = AsyncMock()
return service
@pytest.fixture
def mock_notification_service():
service = AsyncMock()
return service
@pytest.fixture
def order_service(mock_order_repo, mock_payment_service, mock_notification_service):
return OrderService(
order_repo=mock_order_repo,
payment_service=mock_payment_service,
notification_service=mock_notification_service,
)
@pytest.fixture
def valid_order_input():
return CreateOrderInput(
user_id="user-abc-123",
items=[
OrderItem(product_id="prod-001", quantity=2, price_in_cents=2999),
OrderItem(product_id="prod-002", quantity=1, price_in_cents=4999),
],
shipping_address={
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip": "62701",
"country": "US",
},
)
@pytest.fixture
def mock_order():
return Order(
id="order-xyz-789",
user_id="user-abc-123",
status=OrderStatus.PENDING,
total_in_cents=10997,
items=[],
created_at=datetime(2026, 1, 15, 10, 0, 0),
updated_at=datetime(2026, 1, 15, 10, 0, 0),
)
# --- Create Order Tests ---
class TestCreateOrder:
"""Tests for OrderService.create_order"""
async def test_creates_order_with_correct_total(
self, order_service, valid_order_input, mock_order, mock_order_repo, mock_payment_service
):
mock_order_repo.create.return_value = mock_order
mock_payment_service.authorize.return_value = {"transaction_id": "txn-001"}
result = await order_service.create_order(valid_order_input)
assert result.total_in_cents == 10997 # (2999 * 2) + (4999 * 1)
async def test_authorizes_payment_before_saving(
self, order_service, valid_order_input, mock_order, mock_order_repo, mock_payment_service
):
call_order = []
mock_payment_service.authorize.side_effect = lambda *a, **kw: call_order.append("payment")
mock_order_repo.create.side_effect = lambda *a, **kw: (call_order.append("save"), mock_order)[1]
await order_service.create_order(valid_order_input)
assert call_order == ["payment", "save"]
async def test_sends_confirmation_notification(
self, order_service, valid_order_input, mock_order, mock_order_repo,
mock_payment_service, mock_notification_service
):
mock_order_repo.create.return_value = mock_order
mock_payment_service.authorize.return_value = {"transaction_id": "txn-001"}
await order_service.create_order(valid_order_input)
mock_notification_service.send_order_confirmation.assert_called_once()
# Edge Cases
async def test_applies_free_shipping_for_orders_over_100_dollars(
self, order_service, mock_order, mock_order_repo, mock_payment_service
):
input_data = CreateOrderInput(
user_id="user-abc-123",
items=[OrderItem(product_id="prod-001", quantity=1, price_in_cents=15000)],
shipping_address={"country": "US", "zip": "90210"},
)
mock_order_repo.create.return_value = mock_order
mock_payment_service.authorize.return_value = {"transaction_id": "txn-001"}
await order_service.create_order(input_data)
create_call_args = mock_order_repo.create.call_args[0][0]
assert create_call_args.shipping_cost_in_cents == 0
# Error Cases
async def test_raises_validation_error_when_items_empty(self, order_service):
input_data = CreateOrderInput(
user_id="user-abc-123",
items=[],
shipping_address={"country": "US"},
)
with pytest.raises(ValidationError, match="at least one item"):
await order_service.create_order(input_data)
async def test_raises_validation_error_when_quantity_is_zero(self, order_service):
input_data = CreateOrderInput(
user_id="user-abc-123",
items=[OrderItem(product_id="prod-001", quantity=0, price_in_cents=999)],
shipping_address={"country": "US"},
)
with pytest.raises(ValidationError, match="Quantity must be at least 1"):
await order_service.create_order(input_data)
async def test_does_not_save_order_when_payment_fails(
self, order_service, valid_order_input, mock_order_repo, mock_payment_service
):
mock_payment_service.authorize.side_effect = Exception("Card declined")
with pytest.raises(Exception, match="Card declined"):
await order_service.create_order(valid_order_input)
mock_order_repo.create.assert_not_called()
async def test_rolls_back_payment_when_save_fails(
self, order_service, valid_order_input, mock_order_repo, mock_payment_service
):
mock_payment_service.authorize.return_value = {"transaction_id": "txn-001"}
mock_order_repo.create.side_effect = Exception("Database connection lost")
with pytest.raises(Exception, match="Database connection lost"):
await order_service.create_order(valid_order_input)
mock_payment_service.refund.assert_called_once_with("txn-001")
# --- Cancel Order Tests ---
class TestCancelOrder:
"""Tests for OrderService.cancel_order"""
async def test_updates_status_to_cancelled(
self, order_service, mock_order, mock_order_repo
):
mock_order_repo.find_by_id.return_value = mock_order
cancelled_order = Order(**{**mock_order.__dict__, "status": OrderStatus.CANCELLED})
mock_order_repo.update.return_value = cancelled_order
result = await order_service.cancel_order("order-xyz-789", "user-abc-123")
assert result.status == OrderStatus.CANCELLED
async def test_raises_not_found_for_nonexistent_order(
self, order_service, mock_order_repo
):
mock_order_repo.find_by_id.return_value = None
with pytest.raises(NotFoundError, match="Order not found"):
await order_service.cancel_order("nonexistent", "user-abc-123")
async def test_raises_forbidden_when_user_does_not_own_order(
self, order_service, mock_order, mock_order_repo
):
mock_order.user_id = "different-user"
mock_order_repo.find_by_id.return_value = mock_order
with pytest.raises(ForbiddenError):
await order_service.cancel_order("order-xyz-789", "user-abc-123")
async def test_raises_conflict_when_order_already_shipped(
self, order_service, mock_order, mock_order_repo
):
mock_order.status = OrderStatus.SHIPPED
mock_order_repo.find_by_id.return_value = mock_order
with pytest.raises(ConflictError, match="already been shipped"):
await order_service.cancel_order("order-xyz-789", "user-abc-123")
@pytest.mark.parametrize(
"invalid_status",
[OrderStatus.SHIPPED, OrderStatus.DELIVERED, OrderStatus.CANCELLED],
)
async def test_raises_conflict_for_non_cancellable_statuses(
self, order_service, mock_order, mock_order_repo, invalid_status
):
mock_order.status = invalid_status
mock_order_repo.find_by_id.return_value = mock_order
with pytest.raises(ConflictError):
await order_service.cancel_order("order-xyz-789", "user-abc-123")
Go — Standard Testing
package order_test
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"myapp/internal/order"
)
// --- Mocks ---
type MockOrderRepo struct {
mock.Mock
}
func (m *MockOrderRepo) Create(ctx context.Context, o *order.Order) (*order.Order, error) {
args := m.Called(ctx, o)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*order.Order), args.Error(1)
}
func (m *MockOrderRepo) FindByID(ctx context.Context, id string) (*order.Order, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*order.Order), args.Error(1)
}
type MockPaymentService struct {
mock.Mock
}
func (m *MockPaymentService) Authorize(ctx context.Context, amount int, currency string) (string, error) {
args := m.Called(ctx, amount, currency)
return args.String(0), args.Error(1)
}
func (m *MockPaymentService) Refund(ctx context.Context, txnID string) error {
args := m.Called(ctx, txnID)
return args.Error(0)
}
// --- Helpers ---
func newValidInput() order.CreateInput {
return order.CreateInput{
UserID: "user-abc-123",
Items: []order.ItemInput{
{ProductID: "prod-001", Quantity: 2, PriceInCents: 2999},
{ProductID: "prod-002", Quantity: 1, PriceInCents: 4999},
},
ShippingCountry: "US",
}
}
func newMockOrder() *order.Order {
return &order.Order{
ID: "order-xyz-789",
UserID: "user-abc-123",
Status: order.StatusPending,
TotalInCents: 10997,
}
}
// --- Tests ---
func TestCreateOrder(t *testing.T) {
t.Run("creates order with correct total", func(t *testing.T) {
t.Parallel()
repo := new(MockOrderRepo)
payment := new(MockPaymentService)
svc := order.NewService(repo, payment)
payment.On("Authorize", mock.Anything, 10997, "USD").Return("txn-001", nil)
repo.On("Create", mock.Anything, mock.AnythingOfType("*order.Order")).Return(newMockOrder(), nil)
result, err := svc.CreateOrder(context.Background(), newValidInput())
require.NoError(t, err)
assert.Equal(t, 10997, result.TotalInCents)
repo.AssertExpectations(t)
payment.AssertExpectations(t)
})
t.Run("returns error when items slice is empty", func(t *testing.T) {
t.Parallel()
repo := new(MockOrderRepo)
payment := new(MockPaymentService)
svc := order.NewService(repo, payment)
input := newValidInput()
input.Items = []order.ItemInput{}
_, err := svc.CreateOrder(context.Background(), input)
require.Error(t, err)
assert.Contains(t, err.Error(), "at least one item")
repo.AssertNotCalled(t, "Create")
})
t.Run("does not save order when payment fails", func(t *testing.T) {
t.Parallel()
repo := new(MockOrderRepo)
payment := new(MockPaymentService)
svc := order.NewService(repo, payment)
payment.On("Authorize", mock.Anything, mock.Anything, mock.Anything).
Return("", errors.New("card declined"))
_, err := svc.CreateOrder(context.Background(), newValidInput())
require.Error(t, err)
assert.Contains(t, err.Error(), "card declined")
repo.AssertNotCalled(t, "Create")
})
t.Run("refunds payment when save fails", func(t *testing.T) {
t.Parallel()
repo := new(MockOrderRepo)
payment := new(MockPaymentService)
svc := order.NewService(repo, payment)
payment.On("Authorize", mock.Anything, mock.Anything, mock.Anything).Return("txn-001", nil)
repo.On("Create", mock.Anything, mock.Anything).Return(nil, errors.New("db error"))
payment.On("Refund", mock.Anything, "txn-001").Return(nil)
_, err := svc.CreateOrder(context.Background(), newValidInput())
require.Error(t, err)
payment.AssertCalled(t, "Refund", mock.Anything, "txn-001")
})
t.Run("applies free shipping for orders over 10000 cents", func(t *testing.T) {
t.Parallel()
repo := new(MockOrderRepo)
payment := new(MockPaymentService)
svc := order.NewService(repo, payment)
input := order.CreateInput{
UserID: "user-abc-123",
Items: []order.ItemInput{
{ProductID: "prod-001", Quantity: 1, PriceInCents: 15000},
},
ShippingCountry: "US",
}
payment.On("Authorize", mock.Anything, mock.Anything, mock.Anything).Return("txn-001", nil)
repo.On("Create", mock.Anything, mock.MatchedBy(func(o *order.Order) bool {
return o.ShippingCostInCents == 0
})).Return(newMockOrder(), nil)
_, err := svc.CreateOrder(context.Background(), input)
require.NoError(t, err)
repo.AssertExpectations(t)
})
}
func TestCancelOrder(t *testing.T) {
t.Run("cancels pending order successfully", func(t *testing.T) {
t.Parallel()
repo := new(MockOrderRepo)
payment := new(MockPaymentService)
svc := order.NewService(repo, payment)
pending := newMockOrder()
pending.Status = order.StatusPending
repo.On("FindByID", mock.Anything, "order-xyz-789").Return(pending, nil)
repo.On("Update", mock.Anything, mock.Anything).Return(nil)
err := svc.CancelOrder(context.Background(), "order-xyz-789", "user-abc-123")
require.NoError(t, err)
})
t.Run("returns error when order not found", func(t *testing.T) {
t.Parallel()
repo := new(MockOrderRepo)
payment := new(MockPaymentService)
svc := order.NewService(repo, payment)
repo.On("FindByID", mock.Anything, "nonexistent").Return(nil, order.ErrNotFound)
err := svc.CancelOrder(context.Background(), "nonexistent", "user-abc-123")
require.ErrorIs(t, err, order.ErrNotFound)
})
t.Run("returns error when user does not own order", func(t *testing.T) {
t.Parallel()
repo := new(MockOrderRepo)
payment := new(MockPaymentService)
svc := order.NewService(repo, payment)
o := newMockOrder()
o.UserID = "different-user"
repo.On("FindByID", mock.Anything, "order-xyz-789").Return(o, nil)
err := svc.CancelOrder(context.Background(), "order-xyz-789", "user-abc-123")
require.ErrorIs(t, err, order.ErrForbidden)
})
}
// --- Table-Driven Tests ---
func TestCalculateTotal(t *testing.T) {
tests := []struct {
name string
items []order.ItemInput
expected int
}{
{
name: "single item",
items: []order.ItemInput{{Quantity: 1, PriceInCents: 999}},
expected: 999,
},
{
name: "multiple items",
items: []order.ItemInput{{Quantity: 2, PriceInCents: 500}, {Quantity: 1, PriceInCents: 300}},
expected: 1300,
},
{
name: "empty items",
items: []order.ItemInput{},
expected: 0,
},
{
name: "large quantity",
items: []order.ItemInput{{Quantity: 99, PriceInCents: 100}},
expected: 9900,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := order.CalculateTotal(tt.items)
assert.Equal(t, tt.expected, result)
})
}
}
Ruby — RSpec
require 'rails_helper'
RSpec.describe OrderService do
let(:order_repo) { instance_double(OrderRepository) }
let(:payment_service) { instance_double(PaymentService) }
let(:notification_service) { instance_double(NotificationService) }
let(:service) { described_class.new(order_repo:, payment_service:, notification_service:) }
let(:valid_input) do
{
user_id: "user-abc-123",
items: [
{ product_id: "prod-001", quantity: 2, price_in_cents: 2999 },
{ product_id: "prod-002", quantity: 1, price_in_cents: 4999 }
],
shipping_address: { country: "US", zip: "90210" }
}
end
let(:mock_order) do
build(:order,
id: "order-xyz-789",
user_id: "user-abc-123",
status: :pending,
total_in_cents: 10_997
)
end
describe "#create_order" do
context "with valid input" do
before do
allow(payment_service).to receive(:authorize).and_return("txn-001")
allow(order_repo).to receive(:create).and_return(mock_order)
allow(notification_service).to receive(:send_order_confirmation)
end
it "creates order with correct total" do
result = service.create_order(valid_input)
expect(result.total_in_cents).to eq(10_997)
end
it "authorizes payment before saving" do
service.create_order(valid_input)
expect(payment_service).to have_received(:authorize).ordered
expect(order_repo).to have_received(:create).ordered
end
it "sends confirmation notification" do
service.create_order(valid_input)
expect(notification_service).to have_received(:send_order_confirmation)
.with(hash_including(order_id: "order-xyz-789"))
end
end
context "with empty items" do
it "raises ValidationError" do
input = valid_input.merge(items: [])
expect { service.create_order(input) }
.to raise_error(ValidationError, /at least one item/)
end
end
context "when payment fails" do
before do
allow(payment_service).to receive(:authorize).and_raise("Card declined")
end
it "does not save the order" do
expect { service.create_order(valid_input) }.to raise_error(RuntimeError)
expect(order_repo).not_to have_received(:create)
end
end
context "when save fails after payment" do
before do
allow(payment_service).to receive(:authorize).and_return("txn-001")
allow(payment_service).to receive(:refund)
allow(order_repo).to receive(:create).and_raise("DB error")
end
it "refunds the payment" do
expect { service.create_order(valid_input) }.to raise_error(RuntimeError)
expect(payment_service).to have_received(:refund).with("txn-001")
end
end
end
describe "#cancel_order" do
context "when order exists and user owns it" do
let(:pending_order) { build(:order, user_id: "user-abc-123", status: :pending) }
before do
allow(order_repo).to receive(:find_by_id).and_return(pending_order)
allow(order_repo).to receive(:update)
end
it "updates status to cancelled" do
service.cancel_order("order-xyz-789", "user-abc-123")
expect(order_repo).to have_received(:update)
.with(hash_including(status: :cancelled))
end
end
context "when order does not exist" do
before { allow(order_repo).to receive(:find_by_id).and_return(nil) }
it "raises NotFoundError" do
expect { service.cancel_order("nonexistent", "user-abc-123") }
.to raise_error(NotFoundError)
end
end
context "when user does not own the order" do
let(:other_order) { build(:order, user_id: "different-user") }
before { allow(order_repo).to receive(:find_by_id).and_return(other_order) }
it "raises ForbiddenError" do
expect { service.cancel_order("order-xyz-789", "user-abc-123") }
.to raise_error(ForbiddenError)
end
end
%i[shipped delivered cancelled].each do |status|
context "when order status is #{status}" do
let(:order) { build(:order, user_id: "user-abc-123", status:) }
before { allow(order_repo).to receive(:find_by_id).and_return(order) }
it "raises ConflictError" do
expect { service.cancel_order("order-xyz-789", "user-abc-123") }
.to raise_error(ConflictError)
end
end
end
end
end
PHP — PHPUnit / Pest
<?php
use Tests\TestCase;
use App\Services\OrderService;
use App\Models\Order;
use App\Exceptions\ValidationException;
use App\Exceptions\ForbiddenException;
use Illuminate\Foundation\Testing\RefreshDatabase;
class OrderServiceTest extends TestCase
{
use RefreshDatabase;
private OrderService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = app(OrderService::class);
}
private function validInput(array $overrides = []): array
{
return array_merge([
'user_id' => 'user-abc-123',
'items' => [
['product_id' => 'prod-001', 'quantity' => 2, 'price_in_cents' => 2999],
['product_id' => 'prod-002', 'quantity' => 1, 'price_in_cents' => 4999],
],
'shipping_address' => ['country' => 'US', 'zip' => '90210'],
], $overrides);
}
/** @test */
public function it_creates_order_with_correct_total(): void
{
$result = $this->service->createOrder($this->validInput());
$this->assertEquals(10997, $result->total_in_cents);
}
/** @test */
public function it_throws_validation_error_when_items_empty(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('at least one item');
$this->service->createOrder($this->validInput(['items' => []]));
}
/** @test */
public function it_throws_forbidden_when_cancelling_other_users_order(): void
{
$order = Order::factory()->create(['user_id' => 'different-user']);
$this->expectException(ForbiddenException::class);
$this->service->cancelOrder($order->id, 'user-abc-123');
}
/** @test */
public function it_applies_free_shipping_over_100_dollars(): void
{
$input = $this->validInput([
'items' => [
['product_id' => 'prod-001', 'quantity' => 1, 'price_in_cents' => 15000],
],
]);
$result = $this->service->createOrder($input);
$this->assertEquals(0, $result->shipping_cost_in_cents);
}
}
4. API / Integration Test Patterns
REST API Tests — Node.js (Supertest)
import request from 'supertest';
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { app } from '../app';
import { db } from '../db';
import { createTestUser, generateAuthToken } from './helpers';
describe('POST /api/v1/orders', () => {
let authToken: string;
let testUser: { id: string; email: string };
beforeAll(async () => {
await db.migrate.latest();
});
beforeEach(async () => {
await db('orders').truncate();
testUser = await createTestUser();
authToken = generateAuthToken(testUser.id);
});
afterAll(async () => {
await db.destroy();
});
it('creates an order and returns 201', async () => {
const response = await request(app)
.post('/api/v1/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({
items: [{ productId: 'prod-001', quantity: 2 }],
shippingAddress: { country: 'US', zip: '90210' },
});
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
id: expect.any(String),
status: 'pending',
items: expect.arrayContaining([
expect.objectContaining({ productId: 'prod-001', quantity: 2 }),
]),
});
});
it('returns 401 without auth token', async () => {
const response = await request(app)
.post('/api/v1/orders')
.send({ items: [{ productId: 'prod-001', quantity: 1 }] });
expect(response.status).toBe(401);
expect(response.body.error).toBe('UNAUTHORIZED');
});
it('returns 400 with empty items', async () => {
const response = await request(app)
.post('/api/v1/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({ items: [], shippingAddress: { country: 'US' } });
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/at least one item/i);
});
it('returns 400 with invalid product ID', async () => {
const response = await request(app)
.post('/api/v1/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({
items: [{ productId: 'nonexistent', quantity: 1 }],
shippingAddress: { country: 'US' },
});
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/product not found/i);
});
it('returns 429 when rate limit exceeded', async () => {
// Send requests up to the limit
for (let i = 0; i < 100; i++) {
await request(app)
.post('/api/v1/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({ items: [{ productId: 'prod-001', quantity: 1 }] });
}
const response = await request(app)
.post('/api/v1/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({ items: [{ productId: 'prod-001', quantity: 1 }] });
expect(response.status).toBe(429);
});
});
REST API Tests — Python (FastAPI / httpx)
import pytest
from httpx import AsyncClient
from app.main import app
from app.db import get_db, reset_db
from tests.factories import create_test_user, generate_auth_token
@pytest.fixture(autouse=True)
async def clean_db():
await reset_db()
yield
@pytest.fixture
async def client():
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.fixture
async def auth_headers():
user = await create_test_user()
token = generate_auth_token(user.id)
return {"Authorization": f"Bearer {token}"}
class TestCreateOrder:
async def test_creates_order_returns_201(self, client, auth_headers):
response = await client.post(
"/api/v1/orders",
headers=auth_headers,
json={
"items": [{"product_id": "prod-001", "quantity": 2}],
"shipping_address": {"country": "US", "zip": "90210"},
},
)
assert response.status_code == 201
data = response.json()
assert data["status"] == "pending"
assert len(data["items"]) == 1
async def test_returns_401_without_token(self, client):
response = await client.post(
"/api/v1/orders",
json={"items": [{"product_id": "prod-001", "quantity": 1}]},
)
assert response.status_code == 401
async def test_returns_400_with_empty_items(self, client, auth_headers):
response = await client.post(
"/api/v1/orders",
headers=auth_headers,
json={"items": [], "shipping_address": {"country": "US"}},
)
assert response.status_code == 400
assert "at least one item" in response.json()["detail"].lower()
5. Test Checklist by Code Type
For a Service/Business Logic Function
- Happy path with valid input
- Each validation rule (one test per rule)
- Null/undefined/empty input
- Boundary values (min, max, zero, negative)
- Error thrown by each dependency
- Return value shape and types
- Side effects (DB writes, API calls, events emitted)
- Idempotency (if applicable)
- Concurrency (if applicable)
For an API Endpoint
- Success response (status code, body shape)
- Authentication required (401 without token)
- Authorization (403 for wrong role)
- Input validation (400 for each invalid field)
- Resource not found (404)
- Conflict/duplicate (409)
- Rate limiting (429)
- Request body too large (413)
- Correct content-type header
- Pagination (if list endpoint)
- Filtering and sorting (if applicable)
For a UI Component
- Renders without errors
- Displays correct initial state
- Responds to user interactions (click, type, submit)
- Shows loading state
- Shows error state
- Shows empty state
- Form validation (each rule)
- Keyboard navigation / accessibility
- Conditional rendering (shows/hides elements)
- Calls correct callbacks with correct arguments
For a Database Query/Repository
- Returns correct data for valid query
- Returns empty for no matches
- Handles multiple results
- Pagination works correctly
- Filters apply correctly
- Sorting works correctly
- Handles null/optional fields
- Transaction commits on success
- Transaction rolls back on failure
- Concurrent access doesn't corrupt data
For a Utility/Helper Function
- Normal input → correct output
- Edge case inputs (empty string, zero, negative, max int)
- Type coercion (string "123" vs number 123)
- Unicode and special characters
- Very large input
- Null/undefined input
- Return type is consistent
6. Test Data Strategies
Factory Functions (Recommended)
// Create reusable, customizable test data
const createUser = (overrides?: Partial<User>): User => ({
id: `user-${Math.random().toString(36).slice(2, 8)}`,
email: `test-${Date.now()}@example.com`,
name: 'Test User',
role: 'editor',
createdAt: new Date(),
...overrides,
});
// Usage
const admin = createUser({ role: 'admin' });
const inactive = createUser({ status: 'inactive', lastLoginAt: null });
Fixtures for Complex Data
// fixtures/orders.ts
export const sampleOrders = {
simple: { /* ... */ },
withDiscount: { /* ... */ },
international: { /* ... */ },
maxItems: { /* ... */ },
};
Rules for Test Data
- Use realistic data, not "test" and "123"
- Generate unique IDs to prevent collisions
- Keep factories close to test files or in a shared helpers directory
- Override only what's relevant to the test
- Never depend on specific database IDs
Output Format
When generating tests, provide:
1. Test Plan
File: src/services/orderService.ts
Functions to test: createOrder, cancelOrder, getOrderById
Total test cases: 24
createOrder (12 tests):
✅ Happy path: 3 tests
⚠️ Edge cases: 4 tests
❌ Error handling: 5 tests
cancelOrder (8 tests):
✅ Happy path: 2 tests
⚠️ Edge cases: 2 tests
❌ Error handling: 4 tests
getOrderById (4 tests):
✅ Happy path: 1 test
❌ Error handling: 3 tests
2. Complete Test File
The full, runnable test file matching the project's conventions.
3. Setup Instructions
Any additional setup needed:
- New dev dependencies to install
- Test configuration changes
- Mock files to create
- Fixture data to add
4. Run Command
npm test -- --testPathPattern=orderService
pytest tests/test_order_service.py -v
go test ./internal/order/ -v -run TestCreateOrder
Adaptation Rules
- Match existing conventions — read existing test files first and follow the same patterns
- Match naming — use the same naming convention as existing tests (test_, .test., .spec., _test)
- Match structure — co-located tests vs test directory, match what exists
- Match assertion style — expect vs assert, toBe vs toEqual, follow existing patterns
- Use existing helpers — check for test utilities, factories, or fixtures already in the project
- Don't over-mock — test real behavior when possible, mock only external dependencies
- Run the tests — always verify the generated tests actually pass before presenting them
Summary
End every test generation with:
- Tests created — count and file location
- Coverage added — which functions/paths are now covered
- Gaps remaining — what's still untested
- Run command — exact command to run the new tests
- Dependencies needed — any packages to install
Weekly Installs
1
Repository
aakash-dhar/cla…e-skillsFirst Seen
10 days ago
Security Audits
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1