30 KiB
30 KiB
/specweave-testing:test-generate
Generate comprehensive unit, integration, and E2E tests from components, functions, and API endpoints.
You are an expert test generation engineer who creates thorough, production-ready test suites.
Your Task
Analyze code and automatically generate complete test coverage with unit tests, integration tests, and E2E tests.
1. Test Generation Strategy
Unit Tests:
- Test individual functions/methods in isolation
- Mock all external dependencies
- Cover edge cases and error conditions
- Achieve 100% branch coverage
- Fast execution (<10ms per test)
Integration Tests:
- Test component interactions
- Use real dependencies where practical
- Test data flow between modules
- Verify API contracts
- Database integration testing
E2E Tests:
- Test complete user workflows
- Verify real browser behavior
- Test authentication flows
- Check responsive design
- Validate accessibility
2. Unit Test Generation
Function Analysis Example:
// Source: src/utils/formatters.ts
export function formatCurrency(
amount: number,
currency: string = 'USD',
locale: string = 'en-US'
): string {
if (typeof amount !== 'number' || isNaN(amount)) {
throw new Error('Invalid amount: must be a number');
}
if (amount < 0) {
throw new Error('Invalid amount: must be non-negative');
}
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
}
Generated Unit Test:
// Generated: tests/unit/utils/formatters.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency } from '@/utils/formatters';
describe('formatCurrency', () => {
describe('valid inputs', () => {
it('should format USD currency with default locale', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56');
});
it('should format EUR currency', () => {
expect(formatCurrency(1234.56, 'EUR', 'de-DE')).toBe('1.234,56 €');
});
it('should format GBP currency', () => {
expect(formatCurrency(1234.56, 'GBP', 'en-GB')).toBe('£1,234.56');
});
it('should handle zero amount', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('should handle large amounts', () => {
expect(formatCurrency(1000000)).toBe('$1,000,000.00');
});
it('should handle small amounts', () => {
expect(formatCurrency(0.01)).toBe('$0.01');
});
it('should handle floating point precision', () => {
expect(formatCurrency(0.1 + 0.2)).toBe('$0.30');
});
});
describe('edge cases', () => {
it('should throw error for NaN', () => {
expect(() => formatCurrency(NaN)).toThrow('Invalid amount: must be a number');
});
it('should throw error for non-number input', () => {
expect(() => formatCurrency('123' as any)).toThrow('Invalid amount: must be a number');
});
it('should throw error for negative amounts', () => {
expect(() => formatCurrency(-100)).toThrow('Invalid amount: must be non-negative');
});
it('should throw error for undefined', () => {
expect(() => formatCurrency(undefined as any)).toThrow('Invalid amount: must be a number');
});
it('should throw error for null', () => {
expect(() => formatCurrency(null as any)).toThrow('Invalid amount: must be a number');
});
});
describe('locale variations', () => {
it('should use French locale', () => {
expect(formatCurrency(1234.56, 'EUR', 'fr-FR')).toContain('1');
});
it('should use Japanese locale', () => {
expect(formatCurrency(1234, 'JPY', 'ja-JP')).toContain('¥');
});
});
});
3. Component Test Generation
React Component Analysis:
// Source: src/components/Button.tsx
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
loading?: boolean;
icon?: React.ReactNode;
fullWidth?: boolean;
}
export function Button({
children,
onClick,
variant = 'primary',
disabled = false,
loading = false,
icon,
fullWidth = false,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled || loading}
className={`btn btn-${variant} ${fullWidth ? 'w-full' : ''}`}
data-testid="button"
>
{loading && <Spinner />}
{icon && <span className="icon">{icon}</span>}
{children}
</button>
);
}
Generated Component Test:
// Generated: tests/unit/components/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@/tests/utils/test-utils';
import { Button } from '@/components/Button';
describe('Button', () => {
describe('rendering', () => {
it('should render button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('should render primary variant by default', () => {
render(<Button>Click me</Button>);
expect(screen.getByTestId('button')).toHaveClass('btn-primary');
});
it('should render secondary variant', () => {
render(<Button variant="secondary">Click me</Button>);
expect(screen.getByTestId('button')).toHaveClass('btn-secondary');
});
it('should render danger variant', () => {
render(<Button variant="danger">Delete</Button>);
expect(screen.getByTestId('button')).toHaveClass('btn-danger');
});
it('should render with icon', () => {
const icon = <span data-testid="custom-icon">📧</span>;
render(<Button icon={icon}>Send</Button>);
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
});
it('should render full width', () => {
render(<Button fullWidth>Click me</Button>);
expect(screen.getByTestId('button')).toHaveClass('w-full');
});
});
describe('interactions', () => {
it('should call onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByTestId('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('should not call onClick when disabled', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick} disabled>Click me</Button>);
fireEvent.click(screen.getByTestId('button'));
expect(handleClick).not.toHaveBeenCalled();
});
it('should not call onClick when loading', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick} loading>Click me</Button>);
fireEvent.click(screen.getByTestId('button'));
expect(handleClick).not.toHaveBeenCalled();
});
});
describe('states', () => {
it('should be disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByTestId('button')).toBeDisabled();
});
it('should be disabled when loading', () => {
render(<Button loading>Click me</Button>);
expect(screen.getByTestId('button')).toBeDisabled();
});
it('should show spinner when loading', () => {
render(<Button loading>Click me</Button>);
expect(screen.getByTestId('spinner')).toBeInTheDocument();
});
});
describe('accessibility', () => {
it('should have button role', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('should be keyboard accessible', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByTestId('button');
button.focus();
expect(button).toHaveFocus();
});
});
});
4. API Endpoint Test Generation
API Route Analysis:
// Source: src/api/users.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
if (page < 1 || limit < 1 || limit > 100) {
return Response.json(
{ error: 'Invalid pagination parameters' },
{ status: 400 }
);
}
const users = await db.users.findMany({
skip: (page - 1) * limit,
take: limit,
});
return Response.json({ users, page, limit });
}
export async function POST(request: Request) {
const body = await request.json();
const { email, name, role } = body;
if (!email || !name) {
return Response.json(
{ error: 'Email and name are required' },
{ status: 400 }
);
}
const existingUser = await db.users.findUnique({ where: { email } });
if (existingUser) {
return Response.json(
{ error: 'User already exists' },
{ status: 409 }
);
}
const user = await db.users.create({
data: { email, name, role: role || 'user' },
});
return Response.json({ user }, { status: 201 });
}
Generated API Test:
// Generated: tests/integration/api/users.test.ts
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { GET, POST } from '@/api/users';
import { db } from '@/lib/db';
// Mock database
vi.mock('@/lib/db', () => ({
db: {
users: {
findMany: vi.fn(),
findUnique: vi.fn(),
create: vi.fn(),
},
},
}));
describe('Users API', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('GET /api/users', () => {
it('should return users with default pagination', async () => {
const mockUsers = [
{ id: '1', email: 'user1@example.com', name: 'User 1' },
{ id: '2', email: 'user2@example.com', name: 'User 2' },
];
vi.mocked(db.users.findMany).mockResolvedValue(mockUsers);
const request = new Request('http://localhost/api/users');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.users).toEqual(mockUsers);
expect(data.page).toBe(1);
expect(data.limit).toBe(10);
expect(db.users.findMany).toHaveBeenCalledWith({
skip: 0,
take: 10,
});
});
it('should handle custom pagination', async () => {
vi.mocked(db.users.findMany).mockResolvedValue([]);
const request = new Request('http://localhost/api/users?page=2&limit=20');
const response = await GET(request);
const data = await response.json();
expect(data.page).toBe(2);
expect(data.limit).toBe(20);
expect(db.users.findMany).toHaveBeenCalledWith({
skip: 20,
take: 20,
});
});
it('should reject invalid page number', async () => {
const request = new Request('http://localhost/api/users?page=0');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('Invalid pagination parameters');
});
it('should reject limit over 100', async () => {
const request = new Request('http://localhost/api/users?limit=101');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('Invalid pagination parameters');
});
it('should handle database errors', async () => {
vi.mocked(db.users.findMany).mockRejectedValue(new Error('DB error'));
const request = new Request('http://localhost/api/users');
await expect(GET(request)).rejects.toThrow('DB error');
});
});
describe('POST /api/users', () => {
it('should create new user', async () => {
const newUser = {
email: 'new@example.com',
name: 'New User',
role: 'user',
};
vi.mocked(db.users.findUnique).mockResolvedValue(null);
vi.mocked(db.users.create).mockResolvedValue({
id: '123',
...newUser,
});
const request = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(201);
expect(data.user).toMatchObject(newUser);
expect(db.users.create).toHaveBeenCalledWith({
data: newUser,
});
});
it('should default role to user', async () => {
vi.mocked(db.users.findUnique).mockResolvedValue(null);
vi.mocked(db.users.create).mockResolvedValue({
id: '123',
email: 'test@example.com',
name: 'Test',
role: 'user',
});
const request = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com', name: 'Test' }),
});
await POST(request);
expect(db.users.create).toHaveBeenCalledWith({
data: {
email: 'test@example.com',
name: 'Test',
role: 'user',
},
});
});
it('should reject missing email', async () => {
const request = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'Test' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('Email and name are required');
});
it('should reject missing name', async () => {
const request = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('Email and name are required');
});
it('should reject duplicate email', async () => {
vi.mocked(db.users.findUnique).mockResolvedValue({
id: '123',
email: 'existing@example.com',
name: 'Existing',
role: 'user',
});
const request = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({
email: 'existing@example.com',
name: 'Test',
}),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(409);
expect(data.error).toBe('User already exists');
});
});
});
5. Custom Hook Test Generation
Hook Analysis:
// Source: src/hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Generated Hook Test:
// Generated: tests/unit/hooks/useDebounce.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useDebounce } from '@/hooks/useDebounce';
describe('useDebounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should return initial value immediately', () => {
const { result } = renderHook(() => useDebounce('initial', 500));
expect(result.current).toBe('initial');
});
it('should debounce value changes', async () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 500 } }
);
expect(result.current).toBe('initial');
// Update value
rerender({ value: 'updated', delay: 500 });
// Value should not change immediately
expect(result.current).toBe('initial');
// Fast-forward time
vi.advanceTimersByTime(500);
// Value should update after delay
await waitFor(() => {
expect(result.current).toBe('updated');
});
});
it('should cancel previous timeout on rapid changes', async () => {
const { result, rerender } = renderHook(
({ value }) => useDebounce(value, 500),
{ initialProps: { value: 'initial' } }
);
// Rapid updates
rerender({ value: 'update1' });
vi.advanceTimersByTime(200);
rerender({ value: 'update2' });
vi.advanceTimersByTime(200);
rerender({ value: 'final' });
vi.advanceTimersByTime(500);
await waitFor(() => {
expect(result.current).toBe('final');
});
});
it('should handle delay changes', async () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 500 } }
);
rerender({ value: 'updated', delay: 1000 });
vi.advanceTimersByTime(500);
// Should not update yet (new delay is 1000ms)
expect(result.current).toBe('initial');
vi.advanceTimersByTime(500);
await waitFor(() => {
expect(result.current).toBe('updated');
});
});
it('should handle different value types', async () => {
// Number
const { result: numberResult } = renderHook(() => useDebounce(42, 100));
expect(numberResult.current).toBe(42);
// Object
const obj = { name: 'test' };
const { result: objResult } = renderHook(() => useDebounce(obj, 100));
expect(objResult.current).toBe(obj);
// Array
const arr = [1, 2, 3];
const { result: arrResult } = renderHook(() => useDebounce(arr, 100));
expect(arrResult.current).toBe(arr);
});
});
6. Store/State Management Test Generation
Zustand Store Analysis:
// Source: src/stores/useCartStore.ts
import { create } from 'zustand';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: () => number;
}
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => {
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
});
},
removeItem: (id) => {
set((state) => ({
items: state.items.filter((i) => i.id !== id),
}));
},
updateQuantity: (id, quantity) => {
if (quantity <= 0) {
get().removeItem(id);
return;
}
set((state) => ({
items: state.items.map((i) =>
i.id === id ? { ...i, quantity } : i
),
}));
},
clearCart: () => {
set({ items: [] });
},
total: () => {
return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
}));
Generated Store Test:
// Generated: tests/unit/stores/useCartStore.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCartStore } from '@/stores/useCartStore';
describe('useCartStore', () => {
beforeEach(() => {
// Reset store before each test
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.clearCart();
});
});
describe('addItem', () => {
it('should add new item to cart', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({
id: '1',
name: 'Product A',
price: 29.99,
});
});
expect(result.current.items).toHaveLength(1);
expect(result.current.items[0]).toMatchObject({
id: '1',
name: 'Product A',
price: 29.99,
quantity: 1,
});
});
it('should increment quantity for existing item', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({
id: '1',
name: 'Product A',
price: 29.99,
});
result.current.addItem({
id: '1',
name: 'Product A',
price: 29.99,
});
});
expect(result.current.items).toHaveLength(1);
expect(result.current.items[0].quantity).toBe(2);
});
it('should add multiple different items', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.addItem({ id: '2', name: 'Product B', price: 39.99 });
});
expect(result.current.items).toHaveLength(2);
});
});
describe('removeItem', () => {
it('should remove item from cart', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.removeItem('1');
});
expect(result.current.items).toHaveLength(0);
});
it('should not affect other items', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.addItem({ id: '2', name: 'Product B', price: 39.99 });
result.current.removeItem('1');
});
expect(result.current.items).toHaveLength(1);
expect(result.current.items[0].id).toBe('2');
});
it('should handle removing non-existent item', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.removeItem('999');
});
expect(result.current.items).toHaveLength(1);
});
});
describe('updateQuantity', () => {
it('should update item quantity', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.updateQuantity('1', 5);
});
expect(result.current.items[0].quantity).toBe(5);
});
it('should remove item when quantity is 0', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.updateQuantity('1', 0);
});
expect(result.current.items).toHaveLength(0);
});
it('should remove item when quantity is negative', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.updateQuantity('1', -1);
});
expect(result.current.items).toHaveLength(0);
});
});
describe('clearCart', () => {
it('should remove all items', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.addItem({ id: '2', name: 'Product B', price: 39.99 });
result.current.clearCart();
});
expect(result.current.items).toHaveLength(0);
});
});
describe('total', () => {
it('should calculate total for single item', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
});
expect(result.current.total()).toBe(29.99);
});
it('should calculate total for multiple items', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
result.current.addItem({ id: '2', name: 'Product B', price: 39.99 });
});
expect(result.current.total()).toBe(69.98);
});
it('should calculate total with quantities', () => {
const { result } = renderHook(() => useCartStore());
act(() => {
result.current.addItem({ id: '1', name: 'Product A', price: 10 });
result.current.updateQuantity('1', 3);
});
expect(result.current.total()).toBe(30);
});
it('should return 0 for empty cart', () => {
const { result } = renderHook(() => useCartStore());
expect(result.current.total()).toBe(0);
});
});
});
7. Test Generation CLI
Test Generator Script:
// scripts/generate-tests.ts
import fs from 'fs';
import path from 'path';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
interface TestConfig {
sourceFile: string;
testType: 'unit' | 'integration' | 'e2e';
outputFile?: string;
}
export async function generateTests(config: TestConfig) {
const code = fs.readFileSync(config.sourceFile, 'utf-8');
const ast = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
});
const tests: string[] = [];
traverse(ast, {
FunctionDeclaration(path) {
const name = path.node.id?.name;
if (name) {
tests.push(generateFunctionTest(name, path.node));
}
},
ExportNamedDeclaration(path) {
if (path.node.declaration?.type === 'FunctionDeclaration') {
const name = path.node.declaration.id?.name;
if (name) {
tests.push(generateFunctionTest(name, path.node.declaration));
}
}
},
});
const outputPath = config.outputFile || generateOutputPath(config.sourceFile);
const testContent = generateTestFile(tests, config.sourceFile);
fs.writeFileSync(outputPath, testContent, 'utf-8');
console.log(`✓ Generated tests: ${outputPath}`);
}
function generateOutputPath(sourceFile: string): string {
const relativePath = path.relative('src', sourceFile);
return path.join('tests', 'unit', relativePath.replace(/\.ts$/, '.test.ts'));
}
function generateFunctionTest(name: string, node: any): string {
return `
describe('${name}', () => {
it('should work correctly', () => {
// TODO: Implement test
expect(${name}).toBeDefined();
});
});
`;
}
function generateTestFile(tests: string[], sourceFile: string): string {
const importPath = path.relative(
path.dirname(generateOutputPath(sourceFile)),
sourceFile
).replace(/\.ts$/, '');
return `
import { describe, it, expect } from 'vitest';
import { } from '${importPath}';
describe('${path.basename(sourceFile)}', () => {
${tests.join('\n')}
});
`.trim();
}
8. Test Templates
Template Registry (tests/templates/):
// tests/templates/component.template.ts
export const componentTestTemplate = `
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@/tests/utils/test-utils';
import { {{ComponentName}} } from '@/components/{{ComponentName}}';
describe('{{ComponentName}}', () => {
it('should render correctly', () => {
render(<{{ComponentName}} />);
expect(screen.getByTestId('{{testId}}')).toBeInTheDocument();
});
it('should handle user interactions', () => {
const handleClick = vi.fn();
render(<{{ComponentName}} onClick={handleClick} />);
fireEvent.click(screen.getByTestId('{{testId}}'));
expect(handleClick).toHaveBeenCalled();
});
});
`;
// tests/templates/api.template.ts
export const apiTestTemplate = `
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { {{functionName}} } from '@/api/{{fileName}}';
describe('{{functionName}}', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return success response', async () => {
const request = new Request('http://localhost/api/{{endpoint}}');
const response = await {{functionName}}(request);
expect(response.status).toBe(200);
});
it('should handle errors', async () => {
// TODO: Implement error case
});
});
`;
9. Coverage Gaps Detection
Coverage Analyzer:
// scripts/analyze-coverage-gaps.ts
import fs from 'fs';
import path from 'path';
interface CoverageSummary {
total: {
lines: { pct: number };
statements: { pct: number };
functions: { pct: number };
branches: { pct: number };
};
}
export function analyzeCoverageGaps(coverageFile: string) {
const coverage: CoverageSummary = JSON.parse(
fs.readFileSync(coverageFile, 'utf-8')
);
const gaps = [];
Object.entries(coverage).forEach(([file, data]) => {
if (file === 'total') return;
const { lines, functions, branches } = data as any;
if (lines.pct < 80) {
gaps.push({
file,
type: 'lines',
current: lines.pct,
target: 80,
gap: 80 - lines.pct,
});
}
if (functions.pct < 80) {
gaps.push({
file,
type: 'functions',
current: functions.pct,
target: 80,
gap: 80 - functions.pct,
});
}
if (branches.pct < 80) {
gaps.push({
file,
type: 'branches',
current: branches.pct,
target: 80,
gap: 80 - branches.pct,
});
}
});
return gaps.sort((a, b) => b.gap - a.gap);
}
// Generate test stubs for gaps
export function generateGapTests(gaps: any[]) {
gaps.slice(0, 10).forEach((gap) => {
console.log(`
Priority: ${gap.file}
Coverage: ${gap.current}% ${gap.type}
Gap: ${gap.gap}%
Suggested tests:
- Add ${gap.type} coverage for uncovered paths
- Focus on edge cases and error handling
`);
});
}
10. Package Scripts
{
"scripts": {
"test:generate": "tsx scripts/generate-tests.ts",
"test:generate:component": "tsx scripts/generate-tests.ts --type component",
"test:generate:api": "tsx scripts/generate-tests.ts --type api",
"test:coverage:gaps": "tsx scripts/analyze-coverage-gaps.ts coverage/coverage-summary.json",
"test:stub": "tsx scripts/generate-test-stubs.ts"
}
}
Workflow
- Ask about code to generate tests for (file path or pattern)
- Analyze code structure and identify test targets
- Determine appropriate test types (unit/integration/E2E)
- Generate test scaffolding with proper imports
- Create test cases for happy paths
- Add edge case and error handling tests
- Generate mocks and fixtures
- Add accessibility and performance tests (if applicable)
- Run tests to verify they work
- Analyze coverage and suggest improvements
When to Use
- Adding tests to existing code
- Achieving test coverage targets
- Creating test stubs for new features
- Migrating to new testing framework
- Identifying coverage gaps
- Implementing TDD workflow
- Standardizing test patterns
Generate comprehensive test coverage automatically!