Test Suite Generator
Generates comprehensive test suites with unit tests, integration tests, and edge cases. Supports Jest, Vitest, Pytest, and Go testing.
Test Suite Generator
Overview
A Claude Code skill that analyzes your source code and generates comprehensive test suites ā unit tests, integration tests, and edge case coverage. It reads your functions, classes, and modules, understands the business logic, and produces ready-to-run tests with proper mocking, assertions, and test organization.
Quick Start
# Generate tests for a specific file claude "Generate tests for src/services/authService.ts" # Generate tests for an entire directory claude "Write comprehensive tests for all files in src/utils/" # Generate specific test types claude "Write integration tests for the checkout API flow"
What Gets Generated
For each source file, the generator produces:
src/services/authService.ts ā tests/services/authService.test.ts
src/utils/formatCurrency.ts ā tests/utils/formatCurrency.test.ts
src/components/LoginForm.tsx ā tests/components/LoginForm.test.tsx
src/controllers/orderController.ts ā tests/controllers/orderController.test.ts
Test Structure
File Organization
// tests/services/authService.test.ts import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { login, register, refreshToken, logout } from '../../src/services/authService'; // Mocks at the top jest.mock('../../src/db/users'); jest.mock('../../src/lib/jwt'); describe('AuthService', () => { // Setup and teardown beforeEach(() => { jest.clearAllMocks(); }); // Group by function describe('login', () => { // Happy path first it('should return tokens when credentials are valid', async () => { ... }); // Error cases it('should throw when user does not exist', async () => { ... }); it('should throw when password is incorrect', async () => { ... }); it('should throw when account is locked', async () => { ... }); // Edge cases it('should handle case-insensitive email matching', async () => { ... }); it('should trim whitespace from email input', async () => { ... }); }); describe('register', () => { ... }); describe('refreshToken', () => { ... }); describe('logout', () => { ... }); });
Test Patterns
Unit Tests
Test individual functions in isolation:
// Source export function calculateDiscount(price: number, discountPercent: number): number { if (price < 0) throw new Error('Price cannot be negative'); if (discountPercent < 0 || discountPercent > 100) throw new Error('Invalid discount'); return Math.round(price * (1 - discountPercent / 100) * 100) / 100; } // Generated test describe('calculateDiscount', () => { // Happy path it('should apply 10% discount correctly', () => { expect(calculateDiscount(100, 10)).toBe(90); }); it('should apply 50% discount correctly', () => { expect(calculateDiscount(200, 50)).toBe(100); }); it('should handle decimal prices', () => { expect(calculateDiscount(29.99, 15)).toBe(25.49); }); // Boundary values it('should return full price for 0% discount', () => { expect(calculateDiscount(100, 0)).toBe(100); }); it('should return 0 for 100% discount', () => { expect(calculateDiscount(100, 100)).toBe(0); }); it('should handle zero price', () => { expect(calculateDiscount(0, 50)).toBe(0); }); // Error cases it('should throw for negative price', () => { expect(() => calculateDiscount(-10, 10)).toThrow('Price cannot be negative'); }); it('should throw for negative discount', () => { expect(() => calculateDiscount(100, -5)).toThrow('Invalid discount'); }); it('should throw for discount over 100', () => { expect(() => calculateDiscount(100, 150)).toThrow('Invalid discount'); }); // Precision it('should round to 2 decimal places', () => { expect(calculateDiscount(10, 33)).toBe(6.70); }); });
Async Function Tests
// Source export async function fetchUserOrders(userId: string): Promise<Order[]> { const user = await db.users.findById(userId); if (!user) throw new NotFoundError('User not found'); return db.orders.find({ userId, deletedAt: null }).sort({ createdAt: -1 }); } // Generated test describe('fetchUserOrders', () => { it('should return orders sorted by newest first', async () => { mockDb.users.findById.mockResolvedValue({ id: '123', name: 'Test' }); mockDb.orders.find.mockReturnValue({ sort: jest.fn().mockResolvedValue([ { id: 'order-2', createdAt: '2024-02-01' }, { id: 'order-1', createdAt: '2024-01-01' }, ]), }); const orders = await fetchUserOrders('123'); expect(orders).toHaveLength(2); expect(orders[0].id).toBe('order-2'); expect(mockDb.orders.find).toHaveBeenCalledWith({ userId: '123', deletedAt: null, }); }); it('should throw NotFoundError when user does not exist', async () => { mockDb.users.findById.mockResolvedValue(null); await expect(fetchUserOrders('nonexistent')) .rejects.toThrow(NotFoundError); await expect(fetchUserOrders('nonexistent')) .rejects.toThrow('User not found'); }); it('should return empty array when user has no orders', async () => { mockDb.users.findById.mockResolvedValue({ id: '123' }); mockDb.orders.find.mockReturnValue({ sort: jest.fn().mockResolvedValue([]), }); const orders = await fetchUserOrders('123'); expect(orders).toEqual([]); }); });
API Integration Tests
import request from 'supertest'; import app from '../../src/app'; import { createTestUser, generateToken, seedDatabase, cleanDatabase } from '../helpers'; describe('POST /api/orders', () => { let userToken: string; let adminToken: string; beforeAll(async () => { await seedDatabase(); const user = await createTestUser({ role: 'user' }); const admin = await createTestUser({ role: 'admin' }); userToken = generateToken(user); adminToken = generateToken(admin); }); afterAll(async () => { await cleanDatabase(); }); describe('authentication', () => { it('should return 401 without auth token', async () => { const res = await request(app).post('/api/orders').send({ productId: '123' }); expect(res.status).toBe(401); }); it('should return 401 with expired token', async () => { const expiredToken = generateToken({ id: '123' }, { expiresIn: '-1h' }); const res = await request(app) .post('/api/orders') .set('Authorization', `Bearer ${expiredToken}`) .send({ productId: '123' }); expect(res.status).toBe(401); }); }); describe('validation', () => { it('should return 400 when productId is missing', async () => { const res = await request(app) .post('/api/orders') .set('Authorization', `Bearer ${userToken}`) .send({}); expect(res.status).toBe(400); expect(res.body.error).toContain('productId'); }); it('should return 400 when quantity is negative', async () => { const res = await request(app) .post('/api/orders') .set('Authorization', `Bearer ${userToken}`) .send({ productId: '123', quantity: -1 }); expect(res.status).toBe(400); }); }); describe('success', () => { it('should create order and return 201', async () => { const res = await request(app) .post('/api/orders') .set('Authorization', `Bearer ${userToken}`) .send({ productId: 'prod-123', quantity: 2 }); expect(res.status).toBe(201); expect(res.body.data).toMatchObject({ productId: 'prod-123', quantity: 2, status: 'pending', }); expect(res.body.data.id).toBeDefined(); }); it('should deduct inventory on order creation', async () => { const before = await getProductInventory('prod-123'); await request(app) .post('/api/orders') .set('Authorization', `Bearer ${userToken}`) .send({ productId: 'prod-123', quantity: 1 }); const after = await getProductInventory('prod-123'); expect(after).toBe(before - 1); }); }); });
React Component Tests
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LoginForm } from '../../src/components/LoginForm'; describe('LoginForm', () => { const mockOnSubmit = jest.fn(); beforeEach(() => { mockOnSubmit.mockClear(); }); it('should render email and password fields', () => { render(<LoginForm onSubmit={mockOnSubmit} />); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /log in/i })).toBeInTheDocument(); }); it('should submit form with valid credentials', async () => { const user = userEvent.setup(); render(<LoginForm onSubmit={mockOnSubmit} />); await user.type(screen.getByLabelText(/email/i), '[email protected]'); await user.type(screen.getByLabelText(/password/i), 'password123'); await user.click(screen.getByRole('button', { name: /log in/i })); expect(mockOnSubmit).toHaveBeenCalledWith({ email: '[email protected]', password: 'password123', }); }); it('should show validation error for invalid email', async () => { const user = userEvent.setup(); render(<LoginForm onSubmit={mockOnSubmit} />); await user.type(screen.getByLabelText(/email/i), 'not-an-email'); await user.click(screen.getByRole('button', { name: /log in/i })); expect(screen.getByText(/valid email/i)).toBeInTheDocument(); expect(mockOnSubmit).not.toHaveBeenCalled(); }); it('should disable submit button while loading', () => { render(<LoginForm onSubmit={mockOnSubmit} isLoading={true} />); expect(screen.getByRole('button', { name: /logging in/i })).toBeDisabled(); }); it('should display server error message', () => { render(<LoginForm onSubmit={mockOnSubmit} error="Invalid credentials" />); expect(screen.getByRole('alert')).toHaveTextContent('Invalid credentials'); }); });
Mocking Strategies
Module Mocks
// Mock entire modules jest.mock('../../src/db/users', () => ({ findById: jest.fn(), create: jest.fn(), update: jest.fn(), })); // Mock with factory (for classes) jest.mock('../../src/lib/EmailService', () => ({ EmailService: jest.fn().mockImplementation(() => ({ send: jest.fn().mockResolvedValue({ messageId: 'test-id' }), verify: jest.fn().mockResolvedValue(true), })), }));
Partial Mocks
// Mock specific exports, keep the rest real jest.mock('../../src/utils/date', () => ({ ...jest.requireActual('../../src/utils/date'), getCurrentTimestamp: jest.fn(() => new Date('2024-01-15T00:00:00Z')), }));
Spy on Methods
// Spy without replacing implementation const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); // ... run code ... expect(consoleSpy).toHaveBeenCalledWith('Expected error message'); consoleSpy.mockRestore();
Mock External Services
// Mock fetch/axios globally jest.spyOn(global, 'fetch').mockResolvedValue({ ok: true, json: () => Promise.resolve({ data: 'mocked' }), } as Response); // Mock with different responses per call mockFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ id: 1 }) }) // First call .mockResolvedValueOnce({ ok: false, status: 404 }) // Second call .mockRejectedValueOnce(new Error('Network error')); // Third call
Coverage Targets
| Metric | Target | Description |
|---|---|---|
| Statements | > 80% | Percentage of code statements executed |
| Branches | > 75% | Percentage of if/else/switch branches covered |
| Functions | > 85% | Percentage of functions called |
| Lines | > 80% | Percentage of lines executed |
Jest Coverage Configuration
// jest.config.ts { "collectCoverage": true, "coverageDirectory": "coverage", "coverageReporters": ["text", "lcov", "clover"], "coverageThreshold": { "global": { "branches": 75, "functions": 85, "lines": 80, "statements": 80 } }, "collectCoverageFrom": [ "src/**/*.{ts,tsx}", "!src/**/*.d.ts", "!src/**/index.ts", "!src/**/*.stories.tsx" ] }
Test Naming Conventions
Use descriptive names that explain the behavior:
// Pattern: should [expected behavior] when [condition] // Good it('should return 404 when user does not exist', ...); it('should apply discount when coupon is valid', ...); it('should throw ValidationError when email is empty', ...); it('should retry 3 times before failing', ...); // Bad it('test login', ...); it('works', ...); it('handles error', ...);
Edge Cases to Always Test
The generator automatically includes tests for:
- Null/undefined inputs ā What happens with missing data?
- Empty strings and arrays ā Different from null
- Boundary values ā 0, -1, MAX_INT, empty, single item
- Type coercion ā
"5"vs5,truevs"true" - Concurrent operations ā Race conditions with parallel calls
- Large inputs ā Performance with big datasets
- Unicode and special characters ā Names with emojis, RTL text
- Date edge cases ā Timezone boundaries, DST, leap years
- Network failures ā Timeouts, connection errors, retries
Configuration
Jest Configuration
// jest.config.ts import type { Config } from 'jest'; const config: Config = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/tests'], testMatch: ['**/*.test.ts', '**/*.test.tsx'], moduleNameMapper: { '@/(.*)': '<rootDir>/src/$1', }, setupFilesAfterSetup: ['<rootDir>/tests/setup.ts'], testTimeout: 10000, }; export default config;
Test Setup File
// tests/setup.ts import { beforeAll, afterAll, afterEach } from '@jest/globals'; beforeAll(async () => { // Start test database, seed data, etc. }); afterEach(async () => { // Clean up between tests jest.restoreAllMocks(); }); afterAll(async () => { // Close connections, stop containers });
Best Practices
- Test behavior, not implementation ā Tests should verify what code does, not how
- One assertion concept per test ā Each test should verify one logical thing
- Arrange-Act-Assert ā Clear structure: setup ā execute ā verify
- Don't test framework code ā Trust Express, React, etc. to work correctly
- Use factories for test data ā
createTestUser()not inline objects everywhere - Clean up after tests ā Reset mocks, clear databases, restore state
- Avoid testing private methods ā Test through the public API
- Make tests deterministic ā No random data, fixed dates, controlled time
- Test the contract ā Verify inputs produce expected outputs
- Keep tests fast ā Mock I/O, use in-memory databases for unit tests
Reviews
No reviews yet. Be the first to review this template!
Similar Templates
Full-Stack Code Reviewer
Comprehensive code review skill that checks for security vulnerabilities, performance issues, accessibility, and best practices across frontend and backend code.
Pro Architecture Workspace
Battle-tested skill for architectural, decision, making, framework. Includes structured workflows, validation checks, and reusable patterns for development.
Comprehensive Agent Module
Enterprise-grade skill for create, manage, orchestrate, agents. Includes structured workflows, validation checks, and reusable patterns for ai maestro.