Test Writer
Overview
The Test Writer skill produces comprehensive, maintainable tests following established practices: the test pyramid (unit → integration → e2e), the AAA (Arrange-Act-Assert) pattern, meaningful naming conventions, and effective mocking strategies. It helps determine what to test, how to structure test files, what to mock vs. not mock, and how to set coverage targets. Good tests serve as executable documentation that catches regressions before they reach production.
When to Use
- Writing tests for a new function, class, or module
- Adding tests to untested legacy code before refactoring
- Following Test-Driven Development (TDD) — writing tests before implementation
- Evaluating test coverage and identifying what's missing
- Writing integration tests for API endpoints or database interactions
When NOT to Use
- Setting up CI/CD pipelines or test runners (infrastructure concern)
- Load testing or performance benchmarking (different tooling)
- Writing end-to-end browser automation scripts (use a Playwright/Cypress skill)
- Debugging a failing test (use the debugger skill to find the root cause)
Quick Reference
| Level |
Scope |
Speed |
Mock? |
Target % |
| Unit |
Single function/class |
Fast (<1ms) |
Dependencies |
70–80% of tests |
| Integration |
Multiple modules + real DB/API |
Medium (10–100ms) |
External services only |
15–25% of tests |
| E2E |
Full user journey through UI |
Slow (1–30s) |
Nothing |
5–10% of tests |
| Concept |
Description |
| AAA |
Arrange (set up) → Act (call) → Assert (verify) |
| Naming |
describe('unit') / it('should behavior when condition') |
| Mocking |
Replace dependencies with controlled test doubles |
| Coverage target |
80% line coverage; 100% on critical paths |
| TDD cycle |
Red → Green → Refactor |
Instructions
Identify the unit under test
- Pick a single function, method, or class as the subject.
- List all inputs, outputs, side effects, and error conditions.
- Identify dependencies that need to be mocked (databases, HTTP clients, file system, clocks).
Choose the right test level
- Unit test: test a function in isolation with all dependencies mocked.
- Integration test: test multiple real components together (e.g., service + real DB).
- E2E test: test a full user-facing workflow through the actual UI or API.
Identify test cases
- Happy path: the function works correctly with valid input.
- Edge cases: empty string, empty array, zero, null, very large numbers, boundary values.
- Error cases: invalid input, dependency throws, timeout.
- State variations: first-time user vs. returning user; empty list vs. full list.
Apply the AAA pattern to each test
// Arrange — set up test data and mocks
// Act — call the function under test
// Assert — verify the output or side effects
Apply naming conventions
- Describe block: name of the unit under test —
describe('calculateDiscount')
- It block: "should [expected behavior] when [condition]" —
it('should return 0 when cart is empty')
- Test file: co-locate with source or mirror directory structure:
utils.test.ts, utils.spec.py
Mock strategically
- Mock I/O boundaries: databases, HTTP calls, file system, time, random.
- Do NOT mock the thing you're testing.
- Do NOT mock value objects or pure functions — use real instances.
- Prefer dependency injection over module-level patching for testability.
Assert precisely
- Assert on the exact expected value, not just that "something was called."
- Verify side effects: was the correct database method called with the correct args?
- For error cases: assert the exact error type and message.
- Avoid asserting on internal implementation details that could change without breaking behavior.
Check coverage
- Run coverage report; target 80%+ line coverage on business logic.
- 100% coverage on security-critical paths (auth, payment, data validation).
- Untested branches show up as risk — prioritize covering error paths.
Examples
Example 1: Write unit tests for a utility function (JavaScript/Jest)
Input: A calculateDiscount function to test:
// discount.js
export function calculateDiscount(price, userTier) {
if (price <= 0) throw new Error('Price must be positive');
const rates = { silver: 0.05, gold: 0.10, platinum: 0.20 };
const rate = rates[userTier] ?? 0;
return Math.round(price * rate * 100) / 100;
}
Output:
// discount.test.js
import { calculateDiscount } from './discount';
describe('calculateDiscount', () => {
describe('happy path', () => {
it('should apply 5% discount for silver tier', () => {
// Arrange
const price = 100;
const tier = 'silver';
// Act
const result = calculateDiscount(price, tier);
// Assert
expect(result).toBe(5.00);
});
it('should apply 10% discount for gold tier', () => {
expect(calculateDiscount(100, 'gold')).toBe(10.00);
});
it('should apply 20% discount for platinum tier', () => {
expect(calculateDiscount(100, 'platinum')).toBe(20.00);
});
it('should return 0 discount for unknown tier', () => {
expect(calculateDiscount(100, 'bronze')).toBe(0);
});
it('should return 0 discount when tier is undefined', () => {
expect(calculateDiscount(100, undefined)).toBe(0);
});
it('should round to 2 decimal places', () => {
// $99.99 * 5% = 4.9995 → rounds to 5.00
expect(calculateDiscount(99.99, 'silver')).toBe(5.00);
});
});
describe('error cases', () => {
it('should throw when price is zero', () => {
expect(() => calculateDiscount(0, 'gold')).toThrow('Price must be positive');
});
it('should throw when price is negative', () => {
expect(() => calculateDiscount(-50, 'gold')).toThrow('Price must be positive');
});
});
describe('edge cases', () => {
it('should handle very small positive price', () => {
expect(calculateDiscount(0.01, 'platinum')).toBe(0.00);
});
it('should handle very large price', () => {
expect(calculateDiscount(1_000_000, 'platinum')).toBe(200_000);
});
});
});
Example 2: Write an integration test for an API endpoint (Node.js/Jest + Supertest)
Input: An Express endpoint to test:
// routes/users.js
router.post('/users', async (req, res) => {
const { email, name } = req.body;
if (!email) return res.status(422).json({ error: 'email is required' });
const existing = await db.users.findByEmail(email);
if (existing) return res.status(409).json({ error: 'email already in use' });
const user = await db.users.create({ email, name });
return res.status(201).json(user);
});
Output:
// routes/users.integration.test.js
import request from 'supertest';
import { app } from '../app';
import { db } from '../db';
// Use a real test database — no mocking of db layer
beforeAll(async () => { await db.migrate.latest(); });
afterAll(async () => { await db.destroy(); });
afterEach(async () => { await db.raw('DELETE FROM users'); }); // clean state
describe('POST /users', () => {
describe('successful creation', () => {
it('should create a user and return 201 with user data', async () => {
// Arrange
const payload = { email: 'alice@example.com', name: 'Alice' };
// Act
const res = await request(app).post('/users').send(payload);
// Assert
expect(res.status).toBe(201);
expect(res.body).toMatchObject({
id: expect.any(String),
email: 'alice@example.com',
name: 'Alice',
createdAt: expect.any(String),
});
// Verify persisted to DB
const saved = await db.users.findByEmail('alice@example.com');
expect(saved).not.toBeNull();
});
});
describe('validation errors', () => {
it('should return 422 when email is missing', async () => {
const res = await request(app).post('/users').send({ name: 'Bob' });
expect(res.status).toBe(422);
expect(res.body.error).toBe('email is required');
});
});
describe('conflict errors', () => {
it('should return 409 when email already exists', async () => {
// Arrange — seed an existing user
await db.users.create({ email: 'carol@example.com', name: 'Carol' });
// Act
const res = await request(app)
.post('/users')
.send({ email: 'carol@example.com', name: 'Carol 2' });
// Assert
expect(res.status).toBe(409);
expect(res.body.error).toBe('email already in use');
});
});
});
Best Practices
- Write tests before code (TDD) to drive better API design
- One logical assertion concept per test — multiple
expect() calls are fine if they verify the same behavior
- Tests should be deterministic: no random data, no time-dependent behavior (mock
Date.now())
- Keep tests fast — slow tests don't get run; unit tests should complete in milliseconds
- Use test factories or builders for complex object setup to avoid repetition
- Test the contract (what), not the implementation (how) — brittle tests couple to internals
Common Mistakes
- Testing implementation details (private methods, internal state) instead of observable behavior
- Not testing error paths and edge cases — happy-path-only tests miss most bugs
- Over-mocking: mocking so much that tests pass even when real integrations are broken
- Using production database in tests without cleanup — tests pollute each other
- Testing framework code (e.g., testing that Express routing works) instead of your business logic
- Ignoring flaky tests — a flaky test is worse than no test (false confidence)
Tips & Tricks
- Use
test.each (Jest) or @pytest.mark.parametrize for table-driven tests with multiple input/output pairs
- Snapshot tests for complex output structures, but review snapshots on every change
--coverage --collectCoverageFrom to see which lines are untested
- Use
faker or factory-boy to generate realistic test data instead of hand-crafting fixtures
- Jest's
jest.useFakeTimers() / Python's freezegun to control time-dependent tests
Related Skills
1---2name: test-writer3description: Use this skill when writing unit tests, integration tests, or end-to-end tests for existing or new code. Trigger phrases: 'write tests for', 'add test coverage', 'how do I test this', 'TDD this feature'. Not for running or debugging test infrastructure or CI pipelines.4license: MIT5---67# Test Writer89## Overview10The Test Writer skill produces comprehensive, maintainable tests following established practices: the test pyramid (unit → integration → e2e), the AAA (Arrange-Act-Assert) pattern, meaningful naming conventions, and effective mocking strategies. It helps determine what to test, how to structure test files, what to mock vs. not mock, and how to set coverage targets. Good tests serve as executable documentation that catches regressions before they reach production.1112## When to Use13- Writing tests for a new function, class, or module14- Adding tests to untested legacy code before refactoring15- Following Test-Driven Development (TDD) — writing tests before implementation16- Evaluating test coverage and identifying what's missing17- Writing integration tests for API endpoints or database interactions1819## When NOT to Use20- Setting up CI/CD pipelines or test runners (infrastructure concern)21- Load testing or performance benchmarking (different tooling)22- Writing end-to-end browser automation scripts (use a Playwright/Cypress skill)23- Debugging a failing test (use the debugger skill to find the root cause)2425## Quick Reference26| Level | Scope | Speed | Mock? | Target % |27|-------|-------|-------|-------|----------|28| Unit | Single function/class | Fast (<1ms) | Dependencies | 70–80% of tests |29| Integration | Multiple modules + real DB/API | Medium (10–100ms) | External services only | 15–25% of tests |30| E2E | Full user journey through UI | Slow (1–30s) | Nothing | 5–10% of tests |3132| Concept | Description |33|---------|-------------|34| AAA | Arrange (set up) → Act (call) → Assert (verify) |35| Naming | `describe('unit') / it('should behavior when condition')` |36| Mocking | Replace dependencies with controlled test doubles |37| Coverage target | 80% line coverage; 100% on critical paths |38| TDD cycle | Red → Green → Refactor |3940## Instructions41421. **Identify the unit under test**43 - Pick a single function, method, or class as the subject.44 - List all inputs, outputs, side effects, and error conditions.45 - Identify dependencies that need to be mocked (databases, HTTP clients, file system, clocks).46472. **Choose the right test level**48 - **Unit test**: test a function in isolation with all dependencies mocked.49 - **Integration test**: test multiple real components together (e.g., service + real DB).50 - **E2E test**: test a full user-facing workflow through the actual UI or API.51523. **Identify test cases**53 - Happy path: the function works correctly with valid input.54 - Edge cases: empty string, empty array, zero, null, very large numbers, boundary values.55 - Error cases: invalid input, dependency throws, timeout.56 - State variations: first-time user vs. returning user; empty list vs. full list.57584. **Apply the AAA pattern to each test**59 ```60 // Arrange — set up test data and mocks61 // Act — call the function under test62 // Assert — verify the output or side effects63 ```64655. **Apply naming conventions**66 - Describe block: name of the unit under test — `describe('calculateDiscount')`67 - It block: "should [expected behavior] when [condition]" — `it('should return 0 when cart is empty')`68 - Test file: co-locate with source or mirror directory structure: `utils.test.ts`, `utils.spec.py`69706. **Mock strategically**71 - Mock I/O boundaries: databases, HTTP calls, file system, time, random.72 - Do NOT mock the thing you're testing.73 - Do NOT mock value objects or pure functions — use real instances.74 - Prefer dependency injection over module-level patching for testability.75767. **Assert precisely**77 - Assert on the exact expected value, not just that "something was called."78 - Verify side effects: was the correct database method called with the correct args?79 - For error cases: assert the exact error type and message.80 - Avoid asserting on internal implementation details that could change without breaking behavior.81828. **Check coverage**83 - Run coverage report; target 80%+ line coverage on business logic.84 - 100% coverage on security-critical paths (auth, payment, data validation).85 - Untested branches show up as risk — prioritize covering error paths.8687## Examples8889### Example 1: Write unit tests for a utility function (JavaScript/Jest)9091**Input:** A `calculateDiscount` function to test:92```javascript93// discount.js94export function calculateDiscount(price, userTier) {95 if (price <= 0) throw new Error('Price must be positive');96 const rates = { silver: 0.05, gold: 0.10, platinum: 0.20 };97 const rate = rates[userTier] ?? 0;98 return Math.round(price * rate * 100) / 100;99}100```101102**Output:**103```javascript104// discount.test.js105import { calculateDiscount } from './discount';106107describe('calculateDiscount', () => {108 describe('happy path', () => {109 it('should apply 5% discount for silver tier', () => {110 // Arrange111 const price = 100;112 const tier = 'silver';113114 // Act115 const result = calculateDiscount(price, tier);116117 // Assert118 expect(result).toBe(5.00);119 });120121 it('should apply 10% discount for gold tier', () => {122 expect(calculateDiscount(100, 'gold')).toBe(10.00);123 });124125 it('should apply 20% discount for platinum tier', () => {126 expect(calculateDiscount(100, 'platinum')).toBe(20.00);127 });128129 it('should return 0 discount for unknown tier', () => {130 expect(calculateDiscount(100, 'bronze')).toBe(0);131 });132133 it('should return 0 discount when tier is undefined', () => {134 expect(calculateDiscount(100, undefined)).toBe(0);135 });136137 it('should round to 2 decimal places', () => {138 // $99.99 * 5% = 4.9995 → rounds to 5.00139 expect(calculateDiscount(99.99, 'silver')).toBe(5.00);140 });141 });142143 describe('error cases', () => {144 it('should throw when price is zero', () => {145 expect(() => calculateDiscount(0, 'gold')).toThrow('Price must be positive');146 });147148 it('should throw when price is negative', () => {149 expect(() => calculateDiscount(-50, 'gold')).toThrow('Price must be positive');150 });151 });152153 describe('edge cases', () => {154 it('should handle very small positive price', () => {155 expect(calculateDiscount(0.01, 'platinum')).toBe(0.00);156 });157158 it('should handle very large price', () => {159 expect(calculateDiscount(1_000_000, 'platinum')).toBe(200_000);160 });161 });162});163```164165---166167### Example 2: Write an integration test for an API endpoint (Node.js/Jest + Supertest)168169**Input:** An Express endpoint to test:170```javascript171// routes/users.js172router.post('/users', async (req, res) => {173 const { email, name } = req.body;174 if (!email) return res.status(422).json({ error: 'email is required' });175 const existing = await db.users.findByEmail(email);176 if (existing) return res.status(409).json({ error: 'email already in use' });177 const user = await db.users.create({ email, name });178 return res.status(201).json(user);179});180```181182**Output:**183```javascript184// routes/users.integration.test.js185import request from 'supertest';186import { app } from '../app';187import { db } from '../db';188189// Use a real test database — no mocking of db layer190beforeAll(async () => { await db.migrate.latest(); });191afterAll(async () => { await db.destroy(); });192afterEach(async () => { await db.raw('DELETE FROM users'); }); // clean state193194describe('POST /users', () => {195 describe('successful creation', () => {196 it('should create a user and return 201 with user data', async () => {197 // Arrange198 const payload = { email: 'alice@example.com', name: 'Alice' };199200 // Act201 const res = await request(app).post('/users').send(payload);202203 // Assert204 expect(res.status).toBe(201);205 expect(res.body).toMatchObject({206 id: expect.any(String),207 email: 'alice@example.com',208 name: 'Alice',209 createdAt: expect.any(String),210 });211 // Verify persisted to DB212 const saved = await db.users.findByEmail('alice@example.com');213 expect(saved).not.toBeNull();214 });215 });216217 describe('validation errors', () => {218 it('should return 422 when email is missing', async () => {219 const res = await request(app).post('/users').send({ name: 'Bob' });220 expect(res.status).toBe(422);221 expect(res.body.error).toBe('email is required');222 });223 });224225 describe('conflict errors', () => {226 it('should return 409 when email already exists', async () => {227 // Arrange — seed an existing user228 await db.users.create({ email: 'carol@example.com', name: 'Carol' });229230 // Act231 const res = await request(app)232 .post('/users')233 .send({ email: 'carol@example.com', name: 'Carol 2' });234235 // Assert236 expect(res.status).toBe(409);237 expect(res.body.error).toBe('email already in use');238 });239 });240});241```242243## Best Practices244- Write tests before code (TDD) to drive better API design245- One logical assertion concept per test — multiple `expect()` calls are fine if they verify the same behavior246- Tests should be deterministic: no random data, no time-dependent behavior (mock `Date.now()`)247- Keep tests fast — slow tests don't get run; unit tests should complete in milliseconds248- Use test factories or builders for complex object setup to avoid repetition249- Test the contract (what), not the implementation (how) — brittle tests couple to internals250251## Common Mistakes252- Testing implementation details (private methods, internal state) instead of observable behavior253- Not testing error paths and edge cases — happy-path-only tests miss most bugs254- Over-mocking: mocking so much that tests pass even when real integrations are broken255- Using production database in tests without cleanup — tests pollute each other256- Testing framework code (e.g., testing that Express routing works) instead of your business logic257- Ignoring flaky tests — a flaky test is worse than no test (false confidence)258259## Tips & Tricks260- Use `test.each` (Jest) or `@pytest.mark.parametrize` for table-driven tests with multiple input/output pairs261- Snapshot tests for complex output structures, but review snapshots on every change262- `--coverage --collectCoverageFrom` to see which lines are untested263- Use `faker` or `factory-boy` to generate realistic test data instead of hand-crafting fixtures264- Jest's `jest.useFakeTimers()` / Python's `freezegun` to control time-dependent tests265266## Related Skills267- [debugger](../debugger/SKILL.md)268- [code-reviewer](../code-reviewer/SKILL.md)269- [api-designer](../api-designer/SKILL.md)270- [refactorer](../refactorer/SKILL.md)