🧪 Test Writer — Comprehensive Test Generation
Analyze code for untested paths and generate unit, integration, and component tests with proper mocking and edge case coverage.
Activation
When this skill activates, output:
🧪 Test Writer — Generating tests for your codebase...
| Context |
Status |
| User says "write tests", "add tests", "test coverage" |
ACTIVE |
| User wants unit, integration, or component tests |
ACTIVE |
| User mentions mocking, edge cases, or test strategy |
ACTIVE |
| User wants to plan a refactor (tests are part of it) |
DORMANT — see refactor-planner |
| User wants to plan a database migration |
DORMANT — see migration-planner |
Protocol
Step 1: Gather Inputs
Ask the user for:
- Target code: Which files, modules, or features need tests?
- Language/framework: What's the tech stack? (Node/Jest, Python/pytest, React/Vitest, etc.)
- Test runner: What test framework is already configured?
- Existing tests: Are there any tests already? What's the coverage?
- Priority areas: What's most critical to test? (business logic, API routes, UI components)
- External dependencies: What needs mocking? (databases, APIs, file system)
Step 2: Analyze Critical Paths
Identify the most important code paths to test:
| Priority |
Code Path |
Type |
Risk |
Coverage |
| 🔴 Critical |
[business logic / payment flow / auth] |
Unit |
High — bugs here lose money |
None |
| 🟡 Important |
[API routes / data transforms] |
Integration |
Medium — breaks user flows |
Partial |
| 🟢 Standard |
[utility functions / helpers] |
Unit |
Low — isolated, simple |
None |
| 🟢 Standard |
[UI components / forms] |
Component |
Medium — user-facing |
None |
Critical path detection rules:
- Handles money or sensitive data → 🔴 Critical
- Called by 5+ other modules → 🔴 Critical
- Has complex branching (3+ conditions) → 🟡 Important
- Pure function with clear inputs/outputs → 🟢 Standard (but easy to test)
- Recently had bugs → 🔴 Critical regardless of type
Step 3: Generate Unit Tests
For utility functions and business logic:
Test structure:
describe('[ModuleName]', () => {
describe('[functionName]', () => {
// Happy path
it('should [expected behavior] when [condition]', () => {
const result = functionName(validInput);
expect(result).toEqual(expectedOutput);
});
// Edge cases
it('should handle null input gracefully', () => {
expect(() => functionName(null)).not.toThrow();
});
it('should return empty array when given empty input', () => {
const result = functionName([]);
expect(result).toEqual([]);
});
// Boundary values
it('should handle maximum allowed value', () => {
const result = functionName(MAX_VALUE);
expect(result).toBeDefined();
});
it('should handle minimum allowed value', () => {
const result = functionName(MIN_VALUE);
expect(result).toBeDefined();
});
// Error states
it('should throw ValidationError for invalid input', () => {
expect(() => functionName(invalidInput)).toThrow(ValidationError);
});
});
});
Test naming convention:
describe block: Module or class name
- Nested
describe: Function or method name
it block: should [behavior] when [condition]
- Never use
test as a verb in descriptions
Coverage targets per function type:
| Function Type |
Min Coverage |
Key Cases |
| Pure functions |
100% |
All branches, boundary values |
| Business logic |
90% |
Happy path, every error branch |
| Data transforms |
95% |
Null, empty, malformed, large |
| Validators |
100% |
Valid, each invalid case |
Step 4: Generate Integration Tests
For API routes and service interactions:
describe('[Route/Service] Integration', () => {
// Setup
beforeAll(async () => {
await setupTestDatabase();
});
afterEach(async () => {
await cleanupTestData();
});
afterAll(async () => {
await teardownTestDatabase();
});
describe('POST /api/[resource]', () => {
it('should create resource and return 201', async () => {
const response = await request(app)
.post('/api/resource')
.send(validPayload)
.set('Authorization', `Bearer ${testToken}`);
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
id: expect.any(String),
...validPayload,
});
});
it('should return 400 for invalid payload', async () => {
const response = await request(app)
.post('/api/resource')
.send(invalidPayload)
.set('Authorization', `Bearer ${testToken}`);
expect(response.status).toBe(400);
expect(response.body.error).toBeDefined();
});
it('should return 401 without authentication', async () => {
const response = await request(app)
.post('/api/resource')
.send(validPayload);
expect(response.status).toBe(401);
});
});
});
Integration test categories:
| Category |
What to Test |
Setup Needed |
| API routes |
Request/response cycle, status codes, body shape |
Test server, auth tokens |
| Database queries |
CRUD operations, constraints, transactions |
Test database, seed data |
| Service-to-service |
Function calls across module boundaries |
Mocked external services |
| Middleware |
Auth, validation, rate limiting, error handling |
Request mocks |
Step 5: Generate Component Tests
For React/UI components (user-event based):
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ComponentName } from './ComponentName';
describe('<ComponentName />', () => {
const defaultProps = {
onSubmit: vi.fn(),
initialValue: '',
};
it('renders with default props', () => {
render(<ComponentName {...defaultProps} />);
expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument();
});
it('calls onSubmit with form data when submitted', async () => {
const user = userEvent.setup();
render(<ComponentName {...defaultProps} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(defaultProps.onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
});
});
it('shows validation error for invalid input', async () => {
const user = userEvent.setup();
render(<ComponentName {...defaultProps} />);
await user.type(screen.getByLabelText(/email/i), 'not-an-email');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText(/valid email/i)).toBeInTheDocument();
expect(defaultProps.onSubmit).not.toHaveBeenCalled();
});
it('disables submit button while loading', () => {
render(<ComponentName {...defaultProps} isLoading={true} />);
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
});
});
Component testing rules:
- Query by role, label, or text — never by class name or test ID (unless necessary)
- Use
userEvent over fireEvent — simulates real user behavior
- Test user-visible behavior, not implementation details
- Don't test styling — test that elements appear/disappear
- Mock child components only when they have side effects
Step 6: Mock External Dependencies
Provide mocking patterns for common services:
Database (Supabase/Prisma/Drizzle):
// Mock Supabase client
vi.mock('@/lib/supabase', () => ({
supabase: {
from: vi.fn(() => ({
select: vi.fn().mockReturnThis(),
insert: vi.fn().mockReturnThis(),
update: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: mockData, error: null }),
})),
auth: {
getUser: vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null }),
},
},
}));
External APIs (Stripe, SendGrid, etc.):
// Mock Stripe
vi.mock('stripe', () => ({
default: vi.fn(() => ({
customers: {
create: vi.fn().mockResolvedValue({ id: 'cus_test' }),
retrieve: vi.fn().mockResolvedValue(mockCustomer),
},
checkout: {
sessions: {
create: vi.fn().mockResolvedValue({ url: 'https://checkout.stripe.com/test' }),
},
},
})),
}));
Fetch/HTTP:
// Mock global fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
beforeEach(() => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => mockResponse,
status: 200,
});
});
Mocking principles:
- Mock at the boundary — mock the external service, not your wrapper
- Reset mocks between tests (
vi.clearAllMocks() in afterEach)
- Test both success and failure responses from mocks
- Use
mockResolvedValueOnce for sequence-dependent tests
Step 7: Edge Case Coverage
Systematic edge case checklist per input type:
| Input Type |
Edge Cases to Test |
| String |
Empty "", whitespace " ", very long (10000 chars), special chars <>&"', unicode 🎉, SQL injection '; DROP TABLE-- |
| Number |
Zero 0, negative -1, float 0.1 + 0.2, NaN, Infinity, max safe integer |
| Array |
Empty [], single item [x], very large (10000 items), nested arrays, duplicate items |
| Object |
Empty {}, missing required keys, extra unknown keys, nested nulls |
| Date |
Past date, future date, midnight, DST transition, invalid date string, epoch 0 |
| Boolean |
true, false, truthy 1, falsy 0, null, undefined |
| File |
Empty file, very large file, wrong format, corrupted data, missing file |
| Auth |
No token, expired token, invalid token, wrong permissions, admin vs user |
Error state testing:
describe('error handling', () => {
it('should handle network timeout', async () => {
mockFetch.mockRejectedValue(new Error('ETIMEOUT'));
await expect(fetchData()).rejects.toThrow('ETIMEOUT');
});
it('should handle malformed JSON response', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => { throw new SyntaxError('Unexpected token'); },
});
await expect(fetchData()).rejects.toThrow();
});
it('should handle concurrent access', async () => {
const results = await Promise.all([
processItem('item-1'),
processItem('item-2'),
processItem('item-3'),
]);
expect(results).toHaveLength(3);
});
});
Step 8: Test File Organization
Structure test files to mirror source code:
src/
utils/
formatDate.ts → __tests__/utils/formatDate.test.ts
services/
paymentService.ts → __tests__/services/paymentService.test.ts
api/
routes/
users.ts → __tests__/api/routes/users.test.ts
components/
UserForm.tsx → __tests__/components/UserForm.test.tsx
Or colocated pattern:
src/
utils/
formatDate.ts
formatDate.test.ts
components/
UserForm.tsx
UserForm.test.tsx
Follow whichever pattern the project already uses. If no convention exists, recommend colocated.
Step 9: Output
Present the complete test suite:
━━━ TEST SUITE: [Module/Feature Name] ━━━━━
── COVERAGE ANALYSIS ──────────────────────
Critical paths identified: [count]
Current coverage: [%]
Target coverage: [%]
── UNIT TESTS ─────────────────────────────
File: [test file path]
Tests: [count]
[complete test file code]
── INTEGRATION TESTS ──────────────────────
File: [test file path]
Tests: [count]
[complete test file code]
── COMPONENT TESTS ────────────────────────
File: [test file path]
Tests: [count]
[complete test file code]
── MOCKS ──────────────────────────────────
[mock setup files]
── EDGE CASES COVERED ─────────────────────
[checklist of edge cases per input type]
── RUN INSTRUCTIONS ───────────────────────
Command: [test run command]
Watch mode: [watch command]
Coverage report: [coverage command]
Inputs
- Target code files or modules
- Language and test framework
- Current coverage level
- Priority areas (business logic, API, UI)
- External dependencies to mock
Outputs
- Critical path analysis with priority ranking
- Unit tests for utility functions and business logic
- Integration tests for API routes with setup/teardown
- Component tests using user-event patterns
- Mock configurations for external dependencies (Supabase, Stripe, APIs)
- Systematic edge case coverage (null, empty, boundary, error states)
- Complete test files with setup, assertions, and cleanup
- Test naming convention: describe/it with clear behavior descriptions
Level History
- Lv.1 — Base: Critical path analysis, unit test generation with boundary/edge cases, integration tests with setup/teardown, component tests (user-event based), mock patterns for Supabase/Stripe/fetch, systematic edge case checklist per input type, test file organization, describe/it naming convention. (Origin: MemStack v3.2, Mar 2026)
1---2name: test-writer3description: Use when the user says 'write tests', 'add tests', 'test coverage', 'unit tests', 'integration tests', or wants to generate test files for existing code.4---56# 🧪 Test Writer — Comprehensive Test Generation7*Analyze code for untested paths and generate unit, integration, and component tests with proper mocking and edge case coverage.*89## Activation1011When this skill activates, output:1213`🧪 Test Writer — Generating tests for your codebase...`1415| Context | Status |16|---------|--------|17| **User says "write tests", "add tests", "test coverage"** | ACTIVE |18| **User wants unit, integration, or component tests** | ACTIVE |19| **User mentions mocking, edge cases, or test strategy** | ACTIVE |20| **User wants to plan a refactor (tests are part of it)** | DORMANT — see refactor-planner |21| **User wants to plan a database migration** | DORMANT — see migration-planner |2223## Protocol2425### Step 1: Gather Inputs2627Ask the user for:28- **Target code**: Which files, modules, or features need tests?29- **Language/framework**: What's the tech stack? (Node/Jest, Python/pytest, React/Vitest, etc.)30- **Test runner**: What test framework is already configured?31- **Existing tests**: Are there any tests already? What's the coverage?32- **Priority areas**: What's most critical to test? (business logic, API routes, UI components)33- **External dependencies**: What needs mocking? (databases, APIs, file system)3435### Step 2: Analyze Critical Paths3637Identify the most important code paths to test:3839| Priority | Code Path | Type | Risk | Coverage |40|----------|-----------|------|------|----------|41| 🔴 Critical | [business logic / payment flow / auth] | Unit | High — bugs here lose money | None |42| 🟡 Important | [API routes / data transforms] | Integration | Medium — breaks user flows | Partial |43| 🟢 Standard | [utility functions / helpers] | Unit | Low — isolated, simple | None |44| 🟢 Standard | [UI components / forms] | Component | Medium — user-facing | None |4546**Critical path detection rules:**47- Handles money or sensitive data → 🔴 Critical48- Called by 5+ other modules → 🔴 Critical49- Has complex branching (3+ conditions) → 🟡 Important50- Pure function with clear inputs/outputs → 🟢 Standard (but easy to test)51- Recently had bugs → 🔴 Critical regardless of type5253### Step 3: Generate Unit Tests5455For utility functions and business logic:5657**Test structure:**58```javascript59describe('[ModuleName]', () => {60 describe('[functionName]', () => {61 // Happy path62 it('should [expected behavior] when [condition]', () => {63 const result = functionName(validInput);64 expect(result).toEqual(expectedOutput);65 });6667 // Edge cases68 it('should handle null input gracefully', () => {69 expect(() => functionName(null)).not.toThrow();70 });7172 it('should return empty array when given empty input', () => {73 const result = functionName([]);74 expect(result).toEqual([]);75 });7677 // Boundary values78 it('should handle maximum allowed value', () => {79 const result = functionName(MAX_VALUE);80 expect(result).toBeDefined();81 });8283 it('should handle minimum allowed value', () => {84 const result = functionName(MIN_VALUE);85 expect(result).toBeDefined();86 });8788 // Error states89 it('should throw ValidationError for invalid input', () => {90 expect(() => functionName(invalidInput)).toThrow(ValidationError);91 });92 });93});94```9596**Test naming convention:**97- `describe` block: Module or class name98- Nested `describe`: Function or method name99- `it` block: `should [behavior] when [condition]`100- Never use `test` as a verb in descriptions101102**Coverage targets per function type:**103| Function Type | Min Coverage | Key Cases |104|---------------|-------------|-----------|105| Pure functions | 100% | All branches, boundary values |106| Business logic | 90% | Happy path, every error branch |107| Data transforms | 95% | Null, empty, malformed, large |108| Validators | 100% | Valid, each invalid case |109110### Step 4: Generate Integration Tests111112For API routes and service interactions:113114```javascript115describe('[Route/Service] Integration', () => {116 // Setup117 beforeAll(async () => {118 await setupTestDatabase();119 });120121 afterEach(async () => {122 await cleanupTestData();123 });124125 afterAll(async () => {126 await teardownTestDatabase();127 });128129 describe('POST /api/[resource]', () => {130 it('should create resource and return 201', async () => {131 const response = await request(app)132 .post('/api/resource')133 .send(validPayload)134 .set('Authorization', `Bearer ${testToken}`);135136 expect(response.status).toBe(201);137 expect(response.body).toMatchObject({138 id: expect.any(String),139 ...validPayload,140 });141 });142143 it('should return 400 for invalid payload', async () => {144 const response = await request(app)145 .post('/api/resource')146 .send(invalidPayload)147 .set('Authorization', `Bearer ${testToken}`);148149 expect(response.status).toBe(400);150 expect(response.body.error).toBeDefined();151 });152153 it('should return 401 without authentication', async () => {154 const response = await request(app)155 .post('/api/resource')156 .send(validPayload);157158 expect(response.status).toBe(401);159 });160 });161});162```163164**Integration test categories:**165| Category | What to Test | Setup Needed |166|----------|-------------|--------------|167| API routes | Request/response cycle, status codes, body shape | Test server, auth tokens |168| Database queries | CRUD operations, constraints, transactions | Test database, seed data |169| Service-to-service | Function calls across module boundaries | Mocked external services |170| Middleware | Auth, validation, rate limiting, error handling | Request mocks |171172### Step 5: Generate Component Tests173174For React/UI components (user-event based):175176```javascript177import { render, screen } from '@testing-library/react';178import userEvent from '@testing-library/user-event';179import { ComponentName } from './ComponentName';180181describe('<ComponentName />', () => {182 const defaultProps = {183 onSubmit: vi.fn(),184 initialValue: '',185 };186187 it('renders with default props', () => {188 render(<ComponentName {...defaultProps} />);189 expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument();190 });191192 it('calls onSubmit with form data when submitted', async () => {193 const user = userEvent.setup();194 render(<ComponentName {...defaultProps} />);195196 await user.type(screen.getByLabelText(/email/i), 'test@example.com');197 await user.click(screen.getByRole('button', { name: /submit/i }));198199 expect(defaultProps.onSubmit).toHaveBeenCalledWith({200 email: 'test@example.com',201 });202 });203204 it('shows validation error for invalid input', async () => {205 const user = userEvent.setup();206 render(<ComponentName {...defaultProps} />);207208 await user.type(screen.getByLabelText(/email/i), 'not-an-email');209 await user.click(screen.getByRole('button', { name: /submit/i }));210211 expect(screen.getByText(/valid email/i)).toBeInTheDocument();212 expect(defaultProps.onSubmit).not.toHaveBeenCalled();213 });214215 it('disables submit button while loading', () => {216 render(<ComponentName {...defaultProps} isLoading={true} />);217 expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();218 });219});220```221222**Component testing rules:**223- Query by role, label, or text — never by class name or test ID (unless necessary)224- Use `userEvent` over `fireEvent` — simulates real user behavior225- Test user-visible behavior, not implementation details226- Don't test styling — test that elements appear/disappear227- Mock child components only when they have side effects228229### Step 6: Mock External Dependencies230231Provide mocking patterns for common services:232233**Database (Supabase/Prisma/Drizzle):**234```javascript235// Mock Supabase client236vi.mock('@/lib/supabase', () => ({237 supabase: {238 from: vi.fn(() => ({239 select: vi.fn().mockReturnThis(),240 insert: vi.fn().mockReturnThis(),241 update: vi.fn().mockReturnThis(),242 delete: vi.fn().mockReturnThis(),243 eq: vi.fn().mockReturnThis(),244 single: vi.fn().mockResolvedValue({ data: mockData, error: null }),245 })),246 auth: {247 getUser: vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null }),248 },249 },250}));251```252253**External APIs (Stripe, SendGrid, etc.):**254```javascript255// Mock Stripe256vi.mock('stripe', () => ({257 default: vi.fn(() => ({258 customers: {259 create: vi.fn().mockResolvedValue({ id: 'cus_test' }),260 retrieve: vi.fn().mockResolvedValue(mockCustomer),261 },262 checkout: {263 sessions: {264 create: vi.fn().mockResolvedValue({ url: 'https://checkout.stripe.com/test' }),265 },266 },267 })),268}));269```270271**Fetch/HTTP:**272```javascript273// Mock global fetch274const mockFetch = vi.fn();275global.fetch = mockFetch;276277beforeEach(() => {278 mockFetch.mockResolvedValue({279 ok: true,280 json: async () => mockResponse,281 status: 200,282 });283});284```285286**Mocking principles:**287- Mock at the boundary — mock the external service, not your wrapper288- Reset mocks between tests (`vi.clearAllMocks()` in `afterEach`)289- Test both success and failure responses from mocks290- Use `mockResolvedValueOnce` for sequence-dependent tests291292### Step 7: Edge Case Coverage293294Systematic edge case checklist per input type:295296| Input Type | Edge Cases to Test |297|-----------|-------------------|298| **String** | Empty `""`, whitespace `" "`, very long (10000 chars), special chars `<>&"'`, unicode `🎉`, SQL injection `'; DROP TABLE--` |299| **Number** | Zero `0`, negative `-1`, float `0.1 + 0.2`, `NaN`, `Infinity`, max safe integer |300| **Array** | Empty `[]`, single item `[x]`, very large (10000 items), nested arrays, duplicate items |301| **Object** | Empty `{}`, missing required keys, extra unknown keys, nested nulls |302| **Date** | Past date, future date, midnight, DST transition, invalid date string, epoch `0` |303| **Boolean** | `true`, `false`, truthy `1`, falsy `0`, `null`, `undefined` |304| **File** | Empty file, very large file, wrong format, corrupted data, missing file |305| **Auth** | No token, expired token, invalid token, wrong permissions, admin vs user |306307**Error state testing:**308```javascript309describe('error handling', () => {310 it('should handle network timeout', async () => {311 mockFetch.mockRejectedValue(new Error('ETIMEOUT'));312 await expect(fetchData()).rejects.toThrow('ETIMEOUT');313 });314315 it('should handle malformed JSON response', async () => {316 mockFetch.mockResolvedValue({317 ok: true,318 json: async () => { throw new SyntaxError('Unexpected token'); },319 });320 await expect(fetchData()).rejects.toThrow();321 });322323 it('should handle concurrent access', async () => {324 const results = await Promise.all([325 processItem('item-1'),326 processItem('item-2'),327 processItem('item-3'),328 ]);329 expect(results).toHaveLength(3);330 });331});332```333334### Step 8: Test File Organization335336Structure test files to mirror source code:337338```339src/340 utils/341 formatDate.ts → __tests__/utils/formatDate.test.ts342 services/343 paymentService.ts → __tests__/services/paymentService.test.ts344 api/345 routes/346 users.ts → __tests__/api/routes/users.test.ts347 components/348 UserForm.tsx → __tests__/components/UserForm.test.tsx349```350351**Or colocated pattern:**352```353src/354 utils/355 formatDate.ts356 formatDate.test.ts357 components/358 UserForm.tsx359 UserForm.test.tsx360```361362Follow whichever pattern the project already uses. If no convention exists, recommend colocated.363364### Step 9: Output365366Present the complete test suite:367368```369━━━ TEST SUITE: [Module/Feature Name] ━━━━━370371── COVERAGE ANALYSIS ──────────────────────372Critical paths identified: [count]373Current coverage: [%]374Target coverage: [%]375376── UNIT TESTS ─────────────────────────────377File: [test file path]378Tests: [count]379[complete test file code]380381── INTEGRATION TESTS ──────────────────────382File: [test file path]383Tests: [count]384[complete test file code]385386── COMPONENT TESTS ────────────────────────387File: [test file path]388Tests: [count]389[complete test file code]390391── MOCKS ──────────────────────────────────392[mock setup files]393394── EDGE CASES COVERED ─────────────────────395[checklist of edge cases per input type]396397── RUN INSTRUCTIONS ───────────────────────398Command: [test run command]399Watch mode: [watch command]400Coverage report: [coverage command]401```402403## Inputs404- Target code files or modules405- Language and test framework406- Current coverage level407- Priority areas (business logic, API, UI)408- External dependencies to mock409410## Outputs411- Critical path analysis with priority ranking412- Unit tests for utility functions and business logic413- Integration tests for API routes with setup/teardown414- Component tests using user-event patterns415- Mock configurations for external dependencies (Supabase, Stripe, APIs)416- Systematic edge case coverage (null, empty, boundary, error states)417- Complete test files with setup, assertions, and cleanup418- Test naming convention: describe/it with clear behavior descriptions419420## Level History421422- **Lv.1** — Base: Critical path analysis, unit test generation with boundary/edge cases, integration tests with setup/teardown, component tests (user-event based), mock patterns for Supabase/Stripe/fetch, systematic edge case checklist per input type, test file organization, describe/it naming convention. (Origin: MemStack v3.2, Mar 2026)