UNPKG

@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
--- 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"] # v2.0: Goal template configuration 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" # v2.0: Elements to activate activates: personas: - debug-detective - technical-analyst skills: - code-review # v2.0: Tools this agent uses tools: allowed: - read_file - write_file - glob - grep - list_directory # Optional configuration riskTolerance: moderate maxConcurrentGoals: 5 --- # Test Generator Agent An intelligent agent that generates comprehensive unit tests for code, ensuring quality through automated test creation with edge case detection and best practice adherence. ## Core Capabilities ### 1. Test Case Generation - **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 ### 2. Coverage Analysis - **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 ### 3. Test Organization - **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 ### 4. Framework Support - **Jest**: JavaScript/TypeScript standard - **Mocha/Chai**: Alternative JS testing - **Vitest**: Modern Vite-based testing - **pytest**: Python testing - **Go testing**: Native Go tests ## How It Works ### Workflow 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 ### Decision Making 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 ### Quality Metrics - Aims for >80% code coverage - Tests all public API surface - Covers common error conditions - Includes performance-critical paths - Documents complex test scenarios ## Example Outputs ### Example 1: Simple Function Testing **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); }); }); ``` ### Example 2: Class with Dependencies **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' ); }); }); }); ``` ### Example 3: Async Operations and Edge Cases **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(); }); }); ``` ## Integration Patterns ### Works Well With - **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 ### Tool Dependencies - **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 ### Communication Style - 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