T

Test Suite Generator

Generates comprehensive test suites with unit tests, integration tests, and edge cases. Supports Jest, Vitest, Pytest, and Go testing.

SkillClipticstestingv1.0.0MIT
0 views0 copies

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

MetricTargetDescription
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" vs 5, true vs "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

  1. Test behavior, not implementation — Tests should verify what code does, not how
  2. One assertion concept per test — Each test should verify one logical thing
  3. Arrange-Act-Assert — Clear structure: setup → execute → verify
  4. Don't test framework code — Trust Express, React, etc. to work correctly
  5. Use factories for test data — createTestUser() not inline objects everywhere
  6. Clean up after tests — Reset mocks, clear databases, restore state
  7. Avoid testing private methods — Test through the public API
  8. Make tests deterministic — No random data, fixed dates, controlled time
  9. Test the contract — Verify inputs produce expected outputs
  10. Keep tests fast — Mock I/O, use in-memory databases for unit tests
Community

Reviews

Write a review

No reviews yet. Be the first to review this template!

Similar Templates