Sdd-mcp sdd-test-gen
Generate comprehensive tests following TDD methodology. Creates unit tests, integration tests, and edge case coverage. Works with existing test frameworks in the project. Invoked via /sdd-test-gen [file-path or function-name].
git clone https://github.com/yi-john-huang/sdd-mcp
T=$(mktemp -d) && git clone --depth=1 https://github.com/yi-john-huang/sdd-mcp "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/sdd-test-gen" ~/.claude/skills/yi-john-huang-sdd-mcp-sdd-test-gen && rm -rf "$T"
.claude/skills/sdd-test-gen/SKILL.mdSDD Test Generation
Generate comprehensive tests following Test-Driven Development (TDD) methodology. Write tests that serve as living documentation and ensure code correctness.
TDD Philosophy
"Write a failing test before you write the code to make it pass."
Tests are not an afterthought—they're a design tool that:
- Document behavior - Tests show how code is intended to be used
- Prevent regressions - Catch bugs before they ship
- Enable refactoring - Change with confidence
- Drive design - Writing tests first leads to better interfaces
The TDD Cycle
┌─────────────────────────────────────┐ │ │ │ ┌─────────┐ Write failing test │ │ │ RED │◄──────────────────────┤ │ └────┬────┘ │ │ │ │ │ ▼ Make it pass │ │ ┌─────────┐ │ │ │ GREEN │ │ │ └────┬────┘ │ │ │ │ │ ▼ Improve code │ │ ┌─────────┐ │ │ │REFACTOR │───────────────────────┘ │ └─────────┘ └─────────────────────────────────────┘
Workflow
Step 1: Identify Test Scope
/sdd-test-gen src/services/UserService.ts # Generate tests for file /sdd-test-gen UserService.createUser # Generate for specific method /sdd-test-gen src/services/ --integration # Integration tests for module
Step 2: Analyze the Code
Before generating tests:
- Read the source file to understand its behavior
- Check existing tests (if any) to avoid duplication
- Review related requirements in
.spec/specs/ - Identify dependencies that need mocking
Step 3: Test File Structure
Generate tests with this structure:
import { UserService } from '../UserService'; import { UserRepository } from '../../repositories/UserRepository'; import { EmailService } from '../../services/EmailService'; // Mock dependencies jest.mock('../../repositories/UserRepository'); jest.mock('../../services/EmailService'); describe('UserService', () => { let userService: UserService; let mockUserRepo: jest.Mocked<UserRepository>; let mockEmailService: jest.Mocked<EmailService>; beforeEach(() => { jest.clearAllMocks(); mockUserRepo = new UserRepository() as jest.Mocked<UserRepository>; mockEmailService = new EmailService() as jest.Mocked<EmailService>; userService = new UserService(mockUserRepo, mockEmailService); }); describe('createUser', () => { it('should create a user with valid input', async () => { // Arrange const input = { email: 'test@example.com', name: 'Test User' }; mockUserRepo.save.mockResolvedValue({ id: '1', ...input }); // Act const result = await userService.createUser(input); // Assert expect(result.id).toBeDefined(); expect(mockUserRepo.save).toHaveBeenCalledWith(expect.objectContaining(input)); }); it('should throw error when email already exists', async () => { // Arrange mockUserRepo.findByEmail.mockResolvedValue({ id: '1', email: 'test@example.com' }); // Act & Assert await expect(userService.createUser({ email: 'test@example.com' })) .rejects.toThrow('Email already exists'); }); }); });
Step 4: Test Categories to Generate
Unit Tests
Test individual functions in isolation:
- Mock all external dependencies
- Test one behavior per test
- Use descriptive test names
describe('calculateTotal', () => { it('should sum all item prices', () => { ... }); it('should apply discount when provided', () => { ... }); it('should handle empty cart', () => { ... }); it('should throw error for negative quantities', () => { ... }); });
Edge Case Tests
Always test:
- Null/undefined inputs
- Empty arrays/objects
- Boundary values (0, -1, MAX_INT)
- Invalid types
- Concurrent access (if applicable)
describe('edge cases', () => { it('should handle null input gracefully', () => { ... }); it('should handle empty array', () => { ... }); it('should handle maximum allowed length', () => { ... }); it('should reject invalid email format', () => { ... }); });
Error Handling Tests
Verify error paths:
describe('error handling', () => { it('should throw ValidationError for invalid input', async () => { await expect(service.create({})).rejects.toThrow(ValidationError); }); it('should propagate database errors', async () => { mockRepo.save.mockRejectedValue(new DatabaseError('Connection failed')); await expect(service.create(validInput)).rejects.toThrow(DatabaseError); }); });
Integration Tests (when requested)
Test component interactions:
describe('UserService integration', () => { let app: Express; let db: Database; beforeAll(async () => { db = await createTestDatabase(); app = createApp({ database: db }); }); afterAll(async () => { await db.close(); }); it('should create user and send welcome email', async () => { const response = await request(app) .post('/api/users') .send({ email: 'test@example.com', name: 'Test' }); expect(response.status).toBe(201); // Verify email was queued expect(await getEmailQueue()).toContainEqual( expect.objectContaining({ to: 'test@example.com' }) ); }); });
Step 5: Test Naming Convention
Use the pattern:
should [expected behavior] when [condition]
// Good it('should return empty array when no users match criteria', () => {}); it('should throw AuthError when token is expired', () => {}); it('should create user and return ID when input is valid', () => {}); // Bad it('test createUser', () => {}); it('works correctly', () => {});
Test Quality Checklist
For each generated test:
- Test name describes behavior, not implementation
- Arrange-Act-Assert pattern used
- Only one assertion concept per test
- Tests are independent (no shared state)
- Mocks are minimal (only external deps)
- Edge cases covered
- Error paths tested
- Async tests properly awaited
Integration with SDD Workflow
When generating tests for a spec:
- Read requirements from
.spec/specs/{feature}/requirements.md - Map each acceptance criterion to test cases
- Check design.md for expected interfaces
- Reference tasks.md for implementation details
Test Generation Prompt
When running
/sdd-test-gen:
## Test Generation Summary ### Target: {file/function} ### Tests Generated: - **Unit Tests**: {count} - **Edge Cases**: {count} - **Error Handling**: {count} - **Integration**: {count} (if requested) ### Coverage Targets: - Statements: 80%+ - Branches: 75%+ - Functions: 90%+ - Lines: 80%+ ### Files Created: - `src/__tests__/unit/{file}.test.ts` - `src/__tests__/integration/{file}.integration.test.ts` (if requested)
Framework Detection
Automatically detect and use project's test framework:
- Jest (default for TypeScript/JavaScript)
- Vitest (if vite.config.ts present)
- Mocha/Chai (if mocha in dependencies)
- Pytest (for Python projects)
Example Output
For input:
/sdd-test-gen src/services/AuthService.ts
import { AuthService } from '../AuthService'; import { TokenService } from '../../utils/TokenService'; import { UserRepository } from '../../repositories/UserRepository'; jest.mock('../../utils/TokenService'); jest.mock('../../repositories/UserRepository'); describe('AuthService', () => { // ... setup ... describe('login', () => { it('should return token when credentials are valid', async () => { ... }); it('should throw AuthError when user not found', async () => { ... }); it('should throw AuthError when password is incorrect', async () => { ... }); it('should increment failed login count on failure', async () => { ... }); it('should lock account after 5 failed attempts', async () => { ... }); }); describe('logout', () => { it('should invalidate token when called', async () => { ... }); it('should handle already-logged-out user gracefully', async () => { ... }); }); describe('refreshToken', () => { it('should return new token when refresh token is valid', async () => { ... }); it('should throw AuthError when refresh token is expired', async () => { ... }); }); });