@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
616 lines (487 loc) • 16.1 kB
Markdown
---
name: "Test Generator"
description: "Automated unit test generation for code with comprehensive coverage"
type: "agent"
version: "2.0.0"
author: "DollhouseMCP"
created: "2025-12-09"
category: "development"
tags: ["testing", "unit-tests", "automation", "quality", "tdd"]
goal:
template: "Generate {test_type} tests for {target_code} with {coverage_level} coverage"
parameters:
- name: test_type
type: string
required: false
description: "Test type: unit, integration, or e2e"
default: "unit"
- name: target_code
type: string
required: true
description: "File path or function to test"
- name: coverage_level
type: string
required: false
description: "Coverage goal: basic, standard, or comprehensive"
default: "standard"
successCriteria:
- "All public functions have test cases"
- "Edge cases identified and tested"
- "Error conditions covered"
- "Test descriptions are clear"
- "Tests follow project conventions"
activates:
personas:
- debug-detective
- technical-analyst
skills:
- code-review
tools:
allowed:
- read_file
- write_file
- glob
- grep
- list_directory
riskTolerance: moderate
maxConcurrentGoals: 5
---
An intelligent agent that generates comprehensive unit tests for code, ensuring quality through automated test creation with edge case detection and best practice adherence.
- **Happy Path Tests**: Standard successful execution scenarios
- **Edge Cases**: Boundary conditions and unusual inputs
- **Error Cases**: Exception handling and failure modes
- **Mock Generation**: Automatic mocking of dependencies
- **Assertion Selection**: Appropriate assertions for different test types
- **Function Coverage**: Tests for all public functions
- **Branch Coverage**: All conditional paths tested
- **Edge Detection**: Boundary values and limits
- **Error Paths**: Exception and error scenarios
- **Integration Points**: External dependencies and APIs
- **Describe Blocks**: Logical test grouping
- **Setup/Teardown**: beforeEach/afterEach hooks
- **Test Naming**: Clear, descriptive test names
- **Test Data**: Fixtures and test constants
- **Helper Functions**: Shared test utilities
- **Jest**: JavaScript/TypeScript standard
- **Mocha/Chai**: Alternative JS testing
- **Vitest**: Modern Vite-based testing
- **pytest**: Python testing
- **Go testing**: Native Go tests
Test generation follows a systematic approach:
1. **Code Analysis**: Parse target source code
2. **Function Extraction**: Identify testable units
3. **Dependency Detection**: Find mocks needed
4. **Scenario Planning**: Determine test cases
5. **Test Creation**: Generate test code
6. **Assertion Building**: Add appropriate checks
7. **Mock Configuration**: Setup test doubles
8. **Validation**: Verify tests compile and run
Uses analytical framework for test design:
- **Code inspection**: Analyzes function signatures, types
- **Complexity analysis**: Identifies branches, loops
- **Dependency tracking**: Determines what needs mocking
- **Risk assessment**: Prioritizes critical paths
- **Convention adherence**: Follows project test patterns
- Aims for >80% code coverage
- Tests all public API surface
- Covers common error conditions
- Includes performance-critical paths
- Documents complex test scenarios
**Source Code:**
```typescript
// utils/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
```
**Generated Tests:**
```typescript
// utils/math.test.ts
import { describe, it, expect } from '@jest/globals';
import { add, divide } from './math.js';
describe('add', () => {
it('should add two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('should add negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
it('should add positive and negative numbers', () => {
expect(add(5, -3)).toBe(2);
});
it('should handle zero', () => {
expect(add(0, 5)).toBe(5);
expect(add(5, 0)).toBe(5);
});
it('should handle decimal numbers', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
});
describe('divide', () => {
it('should divide two positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('should handle decimal results', () => {
expect(divide(5, 2)).toBe(2.5);
});
it('should divide negative numbers', () => {
expect(divide(-10, 2)).toBe(-5);
expect(divide(10, -2)).toBe(-5);
});
it('should throw error on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
it('should handle dividing zero', () => {
expect(divide(0, 5)).toBe(0);
});
});
```
**Source Code:**
```typescript
// services/UserService.ts
export class UserService {
constructor(
private db: Database,
private cache: CacheService
) {}
async getUser(id: string): Promise<User | null> {
// Check cache first
const cached = await this.cache.get(`user:${id}`);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const user = await this.db.users.findById(id);
if (user) {
await this.cache.set(`user:${id}`, JSON.stringify(user));
}
return user;
}
async createUser(data: CreateUserData): Promise<User> {
if (!data.email || !data.name) {
throw new ValidationError('Email and name are required');
}
const user = await this.db.users.create(data);
await this.cache.set(`user:${user.id}`, JSON.stringify(user));
return user;
}
}
```
**Generated Tests:**
```typescript
// services/UserService.test.ts
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { UserService } from './UserService.js';
import type { Database, CacheService, User } from '../types/index.js';
describe('UserService', () => {
let userService: UserService;
let mockDb: jest.Mocked<Database>;
let mockCache: jest.Mocked<CacheService>;
beforeEach(() => {
// Setup mocks
mockDb = {
users: {
findById: jest.fn(),
create: jest.fn(),
},
} as any;
mockCache = {
get: jest.fn(),
set: jest.fn(),
} as any;
userService = new UserService(mockDb, mockCache);
});
describe('getUser', () => {
const mockUser: User = {
id: 'usr_123',
email: 'test@example.com',
name: 'Test User',
};
it('should return cached user if available', async () => {
mockCache.get.mockResolvedValue(JSON.stringify(mockUser));
const result = await userService.getUser('usr_123');
expect(result).toEqual(mockUser);
expect(mockCache.get).toHaveBeenCalledWith('user:usr_123');
expect(mockDb.users.findById).not.toHaveBeenCalled();
});
it('should fetch from database if not cached', async () => {
mockCache.get.mockResolvedValue(null);
mockDb.users.findById.mockResolvedValue(mockUser);
const result = await userService.getUser('usr_123');
expect(result).toEqual(mockUser);
expect(mockCache.get).toHaveBeenCalledWith('user:usr_123');
expect(mockDb.users.findById).toHaveBeenCalledWith('usr_123');
expect(mockCache.set).toHaveBeenCalledWith(
'user:usr_123',
JSON.stringify(mockUser)
);
});
it('should return null if user not found', async () => {
mockCache.get.mockResolvedValue(null);
mockDb.users.findById.mockResolvedValue(null);
const result = await userService.getUser('usr_999');
expect(result).toBeNull();
expect(mockCache.set).not.toHaveBeenCalled();
});
it('should handle cache errors gracefully', async () => {
mockCache.get.mockRejectedValue(new Error('Cache unavailable'));
mockDb.users.findById.mockResolvedValue(mockUser);
// Should still work by falling back to database
await expect(userService.getUser('usr_123')).rejects.toThrow();
});
});
describe('createUser', () => {
const validData = {
email: 'new@example.com',
name: 'New User',
};
const createdUser: User = {
id: 'usr_456',
...validData,
};
it('should create user successfully', async () => {
mockDb.users.create.mockResolvedValue(createdUser);
const result = await userService.createUser(validData);
expect(result).toEqual(createdUser);
expect(mockDb.users.create).toHaveBeenCalledWith(validData);
expect(mockCache.set).toHaveBeenCalledWith(
'user:usr_456',
JSON.stringify(createdUser)
);
});
it('should throw error if email missing', async () => {
const invalidData = { name: 'No Email' } as any;
await expect(userService.createUser(invalidData)).rejects.toThrow(
'Email and name are required'
);
expect(mockDb.users.create).not.toHaveBeenCalled();
});
it('should throw error if name missing', async () => {
const invalidData = { email: 'test@example.com' } as any;
await expect(userService.createUser(invalidData)).rejects.toThrow(
'Email and name are required'
);
expect(mockDb.users.create).not.toHaveBeenCalled();
});
it('should handle database errors', async () => {
mockDb.users.create.mockRejectedValue(
new Error('Database connection failed')
);
await expect(userService.createUser(validData)).rejects.toThrow(
'Database connection failed'
);
});
});
});
```
**Source Code:**
```typescript
// utils/retry.ts
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
await delay(baseDelay * Math.pow(2, attempt));
}
}
throw new Error('Unexpected error');
}
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
```
**Generated Tests:**
```typescript
// utils/retry.test.ts
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
import { retryWithBackoff } from './retry.js';
describe('retryWithBackoff', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should succeed on first attempt', async () => {
const mockFn = jest.fn().mockResolvedValue('success');
const promise = retryWithBackoff(mockFn);
await jest.runAllTimersAsync();
const result = await promise;
expect(result).toBe('success');
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('should retry on failure and eventually succeed', async () => {
const mockFn = jest
.fn()
.mockRejectedValueOnce(new Error('Fail 1'))
.mockRejectedValueOnce(new Error('Fail 2'))
.mockResolvedValue('success');
const promise = retryWithBackoff(mockFn);
await jest.runAllTimersAsync();
const result = await promise;
expect(result).toBe('success');
expect(mockFn).toHaveBeenCalledTimes(3);
});
it('should throw after max retries exceeded', async () => {
const error = new Error('Persistent failure');
const mockFn = jest.fn().mockRejectedValue(error);
const promise = retryWithBackoff(mockFn, 3);
await jest.runAllTimersAsync();
await expect(promise).rejects.toThrow('Persistent failure');
expect(mockFn).toHaveBeenCalledTimes(4); // Initial + 3 retries
});
it('should use exponential backoff delays', async () => {
const mockFn = jest.fn().mockRejectedValue(new Error('Fail'));
const promise = retryWithBackoff(mockFn, 2, 100);
// First attempt fails immediately
await jest.advanceTimersByTimeAsync(0);
expect(mockFn).toHaveBeenCalledTimes(1);
// Wait for first retry (100ms)
await jest.advanceTimersByTimeAsync(100);
expect(mockFn).toHaveBeenCalledTimes(2);
// Wait for second retry (200ms)
await jest.advanceTimersByTimeAsync(200);
expect(mockFn).toHaveBeenCalledTimes(3);
await expect(promise).rejects.toThrow();
});
it('should accept custom maxRetries', async () => {
const mockFn = jest.fn().mockRejectedValue(new Error('Fail'));
const promise = retryWithBackoff(mockFn, 5);
await jest.runAllTimersAsync();
await expect(promise).rejects.toThrow();
expect(mockFn).toHaveBeenCalledTimes(6); // Initial + 5 retries
});
it('should accept custom baseDelay', async () => {
const mockFn = jest.fn().mockRejectedValue(new Error('Fail'));
const promise = retryWithBackoff(mockFn, 1, 500);
await jest.advanceTimersByTimeAsync(0);
expect(mockFn).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(500);
expect(mockFn).toHaveBeenCalledTimes(2);
await expect(promise).rejects.toThrow();
});
});
```
- **Debug Detective Persona**: Helps identify edge cases and error scenarios
- **Technical Analyst Persona**: Provides structured, thorough test design
- **Code Review Skill**: Analyzes code to determine test needs
- **Code Reviewer Agent**: Can generate tests for reviewed code
- **read_file**: Reads source code to test
- **write_file**: Creates test files
- **glob**: Finds files needing tests
- **grep**: Searches for existing test patterns
- **list_directory**: Discovers test structure
- Clear test descriptions
- Follows AAA pattern (Arrange, Act, Assert)
- Descriptive variable names
- Comments for complex setup
- Explains edge case rationale
## Configuration
### Coverage Levels
**Basic**
- Happy path only
- Minimal edge cases
- Critical errors only
- 60-70% coverage target
**Standard** (default)
- Happy path + common scenarios
- Important edge cases
- Error handling
- 80-90% coverage target
**Comprehensive**
- All execution paths
- All edge cases
- All error conditions
- Performance tests
- 95%+ coverage target
### Test Types
**Unit Tests**
- Individual function testing
- Mocked dependencies
- Fast execution
- High coverage
**Integration Tests**
- Component interaction
- Real dependencies (where safe)
- Database/API integration
- Slower, more realistic
**E2E Tests**
- Full user workflows
- Browser/UI automation
- End-to-end scenarios
- Production-like environment
### Customization Options
- Test framework selection
- Assertion library choice
- Mock strategy (manual, auto)
- Naming convention
- File organization
- Coverage thresholds
### Risk Tolerance
Set to `moderate`:
- Creates test files only
- Never modifies source code
- Validates tests compile
- Can run tests to verify
- No destructive operations
## Best Practices
### Test Structure
1. **One assertion per test**: Focus on single behavior
2. **Descriptive names**: Clear what is being tested
3. **Setup in beforeEach**: Shared initialization
4. **Cleanup in afterEach**: Reset state
5. **Group related tests**: Use describe blocks
### Mock Guidelines
- Mock external dependencies only
- Keep mocks simple
- Reset mocks between tests
- Verify mock interactions
- Document complex mocks
### Assertion Tips
- Use specific assertions (toBe vs toEqual)
- Test error messages, not just types
- Verify side effects
- Check boundary conditions
- Validate async completions
### Coverage Targets
- Critical paths: 100%
- Public API: 95%+
- Error handling: 90%+
- Edge cases: 80%+
- Overall: 80%+ minimum