Building a Test Writer Skill: TDD Automation
Learn to build an AI skill that automatically generates comprehensive tests for your code, supporting TDD workflows and improving test coverage.
Building a Test Writer Skill: TDD Automation
Testing is fundamental to reliable software, yet writing comprehensive tests is time-consuming. A test writer skill can accelerate your testing workflow by automatically generating tests that cover happy paths, edge cases, and error conditions—freeing you to focus on implementation.
In this tutorial, we will build a test writer skill that analyzes code, understands behavior, and generates thorough test suites. Whether you practice TDD or add tests after implementation, this skill will improve your coverage and catch bugs earlier.
What We Are Building
Our test writer skill will:
- Parse code to understand function signatures and behavior
- Identify test scenarios (happy path, edge cases, errors)
- Generate tests using the appropriate testing framework
- Apply testing best practices
- Support multiple testing styles (unit, integration)
By the end, your skill will transform code like this:
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
Into comprehensive tests like this:
describe('divide', () => {
describe('happy path', () => {
it('should divide two positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('should divide negative by positive', () => {
expect(divide(-10, 2)).toBe(-5);
});
it('should return decimal results', () => {
expect(divide(7, 2)).toBe(3.5);
});
});
describe('edge cases', () => {
it('should handle zero dividend', () => {
expect(divide(0, 5)).toBe(0);
});
it('should handle very large numbers', () => {
expect(divide(Number.MAX_SAFE_INTEGER, 1)).toBe(Number.MAX_SAFE_INTEGER);
});
it('should handle very small divisor', () => {
expect(divide(1, 0.0001)).toBeCloseTo(10000);
});
});
describe('error handling', () => {
it('should throw when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
Prerequisites
Before we begin, you should have:
- Understanding of testing concepts and frameworks
- Familiarity with test-driven development
- Basic knowledge of code analysis
- Claude Code installed and configured
Part 1: Architecture Design
Let us design our test writer's approach.
Analysis Pipeline
Code → [Parse] → [Analyze] → [Plan] → [Generate] → Tests
Each stage serves a specific purpose:
## Pipeline Stages
### Stage 1: Parse
- Extract function/class signatures
- Identify parameters and types
- Find return types
- Detect thrown errors
### Stage 2: Analyze
- Understand function purpose
- Identify behavior patterns
- Map input to output relationships
- Find edge cases
### Stage 3: Plan
- Determine test categories
- List specific test cases
- Prioritize by importance
- Plan test structure
### Stage 4: Generate
- Apply framework syntax
- Create test cases
- Add assertions
- Format output
Test Categories
Our skill will generate tests in organized categories:
## Test Categories
### Happy Path Tests
Normal, expected usage:
- Typical inputs
- Common scenarios
- Expected outputs
### Edge Case Tests
Boundary conditions:
- Empty inputs
- Maximum values
- Minimum values
- Boundary transitions
### Error Handling Tests
Failure scenarios:
- Invalid inputs
- Missing data
- Exceptional conditions
- Error messages
### Integration Tests (when applicable)
Component interaction:
- Dependencies
- Side effects
- State changes
Part 2: Building the Parser
The parser extracts structural information from code.
Step 1: Create the Parser Module
---
name: test-parser
description: Parses code to extract testable elements
version: 1.0.0
tools:
- Read
---
# Test Code Parser
Parse source code to extract information needed for test generation.
## Extraction Targets
### Functions
Extract:
```typescript
{
name: string;
parameters: Array<{
name: string;
type: string;
required: boolean;
defaultValue?: any;
}>;
returnType: string;
async: boolean;
throws: string[];
visibility: 'public' | 'private' | 'protected';
}
Classes
Extract:
{
name: string;
constructor: FunctionInfo;
methods: FunctionInfo[];
properties: PropertyInfo[];
staticMembers: Array<FunctionInfo | PropertyInfo>;
}
Dependencies
Identify:
- Imported modules
- Injected dependencies
- External calls
- Side effects
Language Support
TypeScript/JavaScript
// Function patterns
function name(params): ReturnType { }
const name = (params): ReturnType => { }
async function name(params): Promise<Type> { }
// Class patterns
class Name {
constructor(params) { }
method(params): ReturnType { }
static staticMethod(): ReturnType { }
}
// Error patterns
throw new Error('message');
throw new CustomError('message');
Python
# Function patterns
def name(params) -> ReturnType:
"""Docstring"""
async def name(params) -> ReturnType:
"""Docstring"""
# Class patterns
class Name:
def __init__(self, params):
pass
def method(self, params) -> ReturnType:
pass
# Error patterns
raise ValueError('message')
raise CustomError('message')
Output Format
{
"file": "src/utils/math.ts",
"elements": [
{
"type": "function",
"name": "divide",
"parameters": [
{ "name": "a", "type": "number", "required": true },
{ "name": "b", "type": "number", "required": true }
],
"returnType": "number",
"async": false,
"throws": ["Error"],
"visibility": "public",
"body": "...",
"location": { "start": 1, "end": 6 }
}
],
"imports": ["..."],
"exports": ["divide"]
}
## Part 3: Building the Analyzer
The analyzer understands what the code does.
### Step 2: Create the Analyzer Module
```markdown
---
name: test-analyzer
description: Analyzes code to understand behavior for testing
version: 1.0.0
---
# Test Behavior Analyzer
Analyze parsed code to understand behavior and identify test scenarios.
## Analysis Tasks
### 1. Purpose Analysis
Determine what the function does:
- Read function name and body
- Identify the transformation performed
- Understand success conditions
- Map input to output
### 2. Input Analysis
For each parameter:
```markdown
Identify:
- Valid value ranges
- Special values (null, empty, zero)
- Boundary values
- Invalid values
- Type constraints
3. Output Analysis
For the return value:
Identify:
- Possible return values
- Return conditions
- Side effects
- State changes
4. Error Analysis
For thrown errors:
Identify:
- Trigger conditions
- Error types
- Error messages
- Recovery options
5. Dependency Analysis
For external calls:
Identify:
- Mock requirements
- Stub behavior
- Verification points
Test Case Identification
Happy Path Cases
Based on purpose analysis:
happyPath:
- scenario: "typical usage"
inputs: [representative values]
expected: normal output
- scenario: "alternative valid path"
inputs: [different valid values]
expected: corresponding output
Edge Cases
Based on input analysis:
edgeCases:
- scenario: "minimum values"
inputs: [smallest valid inputs]
expected: boundary output
- scenario: "maximum values"
inputs: [largest valid inputs]
expected: boundary output
- scenario: "empty/zero values"
inputs: [empty or zero where valid]
expected: corresponding output
- scenario: "boundary transition"
inputs: [values at boundaries]
expected: correct behavior
Error Cases
Based on error analysis:
errorCases:
- scenario: "invalid input"
inputs: [invalid values]
expected: specific error
- scenario: "missing required"
inputs: [missing required params]
expected: validation error
- scenario: "constraint violation"
inputs: [values violating constraints]
expected: constraint error
Output Format
{
"element": "divide",
"analysis": {
"purpose": "Divides first number by second",
"inputs": {
"a": {
"type": "number",
"validRange": "any number",
"specialValues": [0, "negative", "decimal"]
},
"b": {
"type": "number",
"validRange": "non-zero",
"invalidValues": [0],
"specialValues": ["negative", "decimal", "very small"]
}
},
"output": {
"type": "number",
"range": "any number including infinity"
},
"errors": [
{
"condition": "b === 0",
"type": "Error",
"message": "Division by zero"
}
]
},
"testCases": {
"happyPath": [
{ "name": "divides positive numbers", "inputs": [10, 2], "expected": 5 },
{ "name": "handles negative dividend", "inputs": [-10, 2], "expected": -5 },
{ "name": "handles decimal result", "inputs": [7, 2], "expected": 3.5 }
],
"edgeCases": [
{ "name": "zero dividend", "inputs": [0, 5], "expected": 0 },
{ "name": "very large numbers", "inputs": [1e15, 1], "expected": 1e15 },
{ "name": "very small divisor", "inputs": [1, 0.0001], "expected": 10000 }
],
"errorCases": [
{ "name": "division by zero", "inputs": [10, 0], "throws": "Division by zero" }
]
}
}
## Part 4: Building the Planner
The planner organizes test cases into a structured plan.
### Step 3: Create the Planner Module
```markdown
---
name: test-planner
description: Plans test structure and organization
version: 1.0.0
---
# Test Planner
Organize analyzed test cases into a structured test plan.
## Planning Tasks
### 1. Test Structure
Organize tests hierarchically:
describe('functionName', () => { describe('happy path', () => { it('test case 1') it('test case 2') })
describe('edge cases', () => { it('test case 1') it('test case 2') })
describe('error handling', () => { it('test case 1') }) })
### 2. Test Prioritization
Order tests by importance:
1. Critical path tests (most common usage)
2. Error handling (prevent crashes)
3. Edge cases (boundary behavior)
4. Performance considerations
### 3. Setup Requirements
Identify test setup needs:
```yaml
setup:
beforeAll: [one-time setup]
beforeEach: [per-test setup]
afterEach: [per-test cleanup]
afterAll: [final cleanup]
mocks:
- module: 'dependency'
behavior: 'return value'
fixtures:
- name: 'testData'
value: { ... }
4. Assertion Strategy
Choose appropriate assertions:
assertions:
equality:
- toBe: exact match
- toEqual: deep equality
numeric:
- toBeCloseTo: floating point
- toBeGreaterThan: comparison
- toBeLessThan: comparison
exceptions:
- toThrow: any error
- toThrow('message'): specific message
- toThrowError(Type): specific type
async:
- resolves.toBe: successful promise
- rejects.toThrow: failed promise
Output Format
{
"element": "divide",
"testPlan": {
"structure": {
"describe": "divide",
"groups": [
{
"describe": "happy path",
"tests": [
{
"it": "should divide two positive numbers",
"inputs": [10, 2],
"assertion": { "type": "toBe", "expected": 5 }
}
]
},
{
"describe": "edge cases",
"tests": [...]
},
{
"describe": "error handling",
"tests": [
{
"it": "should throw when dividing by zero",
"inputs": [10, 0],
"assertion": { "type": "toThrow", "expected": "Division by zero" }
}
]
}
]
},
"setup": {
"imports": ["divide"],
"mocks": [],
"fixtures": []
}
}
}
## Part 5: Building the Generator
The generator produces the actual test code.
### Step 4: Create the Generator Module
```markdown
---
name: test-generator
description: Generates test code from test plans
version: 1.0.0
tools:
- Write
---
# Test Code Generator
Generate test code from structured test plans.
## Framework Support
### Jest (JavaScript/TypeScript)
```typescript
import { functionName } from './module';
describe('functionName', () => {
describe('happy path', () => {
it('should do something', () => {
expect(functionName(input)).toBe(expected);
});
});
});
Pytest (Python)
import pytest
from module import function_name
class TestFunctionName:
class TestHappyPath:
def test_should_do_something(self):
assert function_name(input) == expected
Vitest (Modern JavaScript)
import { describe, it, expect } from 'vitest';
import { functionName } from './module';
describe('functionName', () => {
it('should do something', () => {
expect(functionName(input)).toBe(expected);
});
});
Generation Templates
Basic Test
it('{testName}', () => {
{setup}
const result = {functionCall};
expect(result).{assertion}({expected});
});
Async Test
it('{testName}', async () => {
{setup}
const result = await {functionCall};
expect(result).{assertion}({expected});
});
Error Test
it('{testName}', () => {
expect(() => {functionCall}).toThrow({expectedError});
});
Async Error Test
it('{testName}', async () => {
await expect({functionCall}).rejects.toThrow({expectedError});
});
Mock Test
it('{testName}', () => {
const mockDep = jest.fn().mockReturnValue({mockValue});
{setup with mock}
const result = {functionCall};
expect(mockDep).toHaveBeenCalledWith({expectedArgs});
expect(result).{assertion}({expected});
});
Formatting Rules
Naming Conventions
Test names should:
- Start with "should"
- Describe the expected behavior
- Be readable as a sentence
Examples:
- "should return the sum of two numbers"
- "should throw when input is null"
- "should call the API with correct parameters"
Code Style
- Consistent indentation (2 spaces)
- Blank lines between test groups
- Comments for complex setups
- Descriptive variable names
Organization
Order within describe blocks:
- Setup (beforeEach, etc.)
- Happy path tests
- Edge case tests
- Error tests
- Cleanup (afterEach, etc.)
Output
Write generated tests to appropriate file:
- Input:
src/utils/math.ts - Output:
src/utils/math.test.tsortests/utils/math.test.ts
## Part 6: Complete Skill Integration
### Step 5: Create the Main Skill
```markdown
---
name: test-writer
description: Automatically generates comprehensive tests for code
version: 1.0.0
tools:
- Read
- Write
- Glob
arguments:
- name: file
type: string
required: true
description: File or pattern to generate tests for
- name: framework
type: string
default: jest
options: [jest, vitest, pytest, mocha]
description: Testing framework to use
- name: style
type: string
default: comprehensive
options: [minimal, standard, comprehensive]
description: Test coverage level
- name: output
type: string
description: Output file path (default: alongside source)
---
# Test Writer
Automatically generate comprehensive tests for your code.
## Workflow
### Phase 1: Parse
Read and parse the target file(s):
1. Load file contents
2. Parse code structure
3. Extract functions, classes, methods
4. Identify signatures and types
### Phase 2: Analyze
Understand code behavior:
1. Determine function purposes
2. Identify input ranges
3. Find edge cases
4. Detect error conditions
### Phase 3: Plan
Organize test strategy:
1. Create test structure
2. Plan test cases by category
3. Determine setup needs
4. Choose assertions
### Phase 4: Generate
Create test code:
1. Apply framework templates
2. Generate test cases
3. Add setup and teardown
4. Format output
### Phase 5: Write
Output the tests:
1. Determine output path
2. Write test file
3. Report coverage summary
## Test Coverage Levels
### Minimal
- One happy path test per function
- One error test if function throws
### Standard
- Multiple happy path scenarios
- Key edge cases
- All error conditions
### Comprehensive
- Thorough happy path coverage
- Extensive edge cases
- All error conditions
- Boundary testing
- Type checking (if applicable)
## Framework Configurations
### Jest
```typescript
// jest.config.js compatible
// Uses describe/it/expect
// Supports mocking with jest.fn()
Vitest
// vitest.config.ts compatible
// Uses describe/it/expect from vitest
// Supports modern ESM
Pytest
# pytest compatible
# Uses class-based or function-based tests
# Supports fixtures
Example Usage
# Generate tests for a single file
/test-writer --file src/utils/math.ts
# Generate with specific framework
/test-writer --file src/api/users.ts --framework vitest
# Generate minimal tests
/test-writer --file src/services/*.ts --style minimal
# Specify output location
/test-writer --file src/core/auth.ts --output tests/core/auth.test.ts
Output
Complete test file with:
- Imports and setup
- Organized test groups
- Comprehensive test cases
- Appropriate assertions
- Clean formatting
Summary Report
After generation:
Tests Generated for: src/utils/math.ts
Output: src/utils/math.test.ts
Coverage:
- Functions: 3
- Test Cases: 15
- Happy Path: 6
- Edge Cases: 6
- Error Cases: 3
Run tests with: npm test src/utils/math.test.ts
## Part 7: Advanced Features
### Step 6: Add TDD Support
Support test-first development:
```markdown
## TDD Mode
When given a function signature (not implementation):
### Input
```typescript
// Function to implement
function calculateTax(amount: number, rate: number): number;
Output
describe('calculateTax', () => {
it('should calculate tax for positive amount', () => {
expect(calculateTax(100, 0.1)).toBe(10);
});
it('should handle zero amount', () => {
expect(calculateTax(0, 0.1)).toBe(0);
});
it('should handle zero rate', () => {
expect(calculateTax(100, 0)).toBe(0);
});
// Tests drive implementation requirements
});
Workflow
- Write tests first (from signature)
- Tests initially fail
- Implement to make tests pass
- Refactor with confidence
### Step 7: Add Mock Generation
Generate mocks for dependencies:
```markdown
## Mock Generation
When function has dependencies:
### Analysis
```typescript
// Function with dependency
async function fetchUserData(userId: string): Promise<User> {
const response = await api.get(`/users/${userId}`);
return response.data;
}
Generated Tests
describe('fetchUserData', () => {
const mockApi = {
get: jest.fn()
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should fetch user by id', async () => {
const mockUser = { id: '123', name: 'Test User' };
mockApi.get.mockResolvedValue({ data: mockUser });
const result = await fetchUserData('123');
expect(mockApi.get).toHaveBeenCalledWith('/users/123');
expect(result).toEqual(mockUser);
});
it('should handle API error', async () => {
mockApi.get.mockRejectedValue(new Error('Network error'));
await expect(fetchUserData('123')).rejects.toThrow('Network error');
});
});
## Testing Your Skill
Verify the skill works correctly:
```markdown
## Test Cases
### Test 1: Simple Function
Input: Pure function with types
Expected: Complete test coverage
### Test 2: Async Function
Input: Function returning Promise
Expected: Async tests with await
### Test 3: Class with Methods
Input: Class definition
Expected: Tests for constructor and methods
### Test 4: Function with Dependencies
Input: Function calling external services
Expected: Tests with mocks
### Test 5: Error-Throwing Function
Input: Function with error conditions
Expected: Error tests with toThrow
Conclusion
You have built a comprehensive test writer skill that:
- Parses code to extract testable elements
- Analyzes behavior to identify test scenarios
- Plans test structure and organization
- Generates framework-specific test code
- Supports multiple frameworks and styles
This skill demonstrates the Chain pattern with tool integration, showing how to build complex functionality through focused stages.
Key takeaways:
- Parse before analyzing for structured data
- Identify test categories systematically
- Generate framework-appropriate code
- Support different coverage levels
- Enable TDD workflows
Your test writer is ready to accelerate your testing workflow!