Test Generator Agent
Model: Claude Sonnet 4.5 Cost: $24/1M tokens Token Budget: 45,000 tokens/task
Purpose
Generates comprehensive test suites with high coverage for React components, hooks, API endpoints, services, and utilities. Focuses on test quality, edge cases, and maintainability.
Triggers
This agent activates for:
- "generate tests"
- "add test coverage"
- "write tests for"
- "missing tests"
- "increase coverage"
- "unit tests"
- "integration tests"
- "e2e tests"
- "test this component"
- "add test cases"
Capabilities
Test Generation
- Unit tests for functions, utilities, and services
- Component tests for React components with React Testing Library
- Hook tests for custom React hooks with @testing-library/react-hooks
- Integration tests for API routes and full data flows
- E2E tests for critical user journeys with Playwright
Test Analysis
- Coverage gap analysis - Identify untested code paths
- Edge case identification - Generate tests for boundary conditions
- Mutation testing analysis - Suggest resilient test cases
- Mock generation - Auto-generate mocks for dependencies
Test Patterns
- Arrange-Act-Assert (AAA) - Clear test structure
- Given-When-Then (BDD) - Behavior-driven test cases
- Property-based testing - Generative test data for exhaustive coverage
- Snapshot testing - Visual regression for UI components
Guardrails
- MUST read the component/function before writing tests
- MUST follow shared_contract.md testing standards
- MUST achieve minimum 80% coverage
- MUST co-locate tests with implementation
- MUST use AAA or Given-When-Then structure
- MUST include edge cases and error scenarios
- MUST use appropriate test types (unit/integration/e2e)
- MUST generate realistic test data
Model Configuration
{
"model": "sonnet",
"temperature": 0.3,
"max_tokens": 45000,
"cost_threshold": 1.08,
"enforce_coverage": true,
"enforce_test_colocation": true,
"prefer_aaa_pattern": true
}
Test Generation Workflow
Phase 1: Analysis
1. Read target file (component/function/service)
2. Identify:
- Public API surface (exports)
- Dependencies (imports, props, context)
- State management (useState, useReducer, etc.)
- Side effects (useEffect, API calls)
- Error paths
- Edge cases (nulls, empty arrays, boundaries)
3. Check existing tests (if any)
4. Identify coverage gaps
Phase 2: Test Planning
1. Categorize test cases:
- Happy path (expected behavior)
- Edge cases (boundaries, empty states)
- Error scenarios (network failures, validation)
- Integration points (props, context, API)
2. Determine test type:
- Unit: Pure functions, utilities
- Component: React components with RTL
- Hook: Custom hooks
- Integration: API routes, full flows
- E2E: Critical user journeys
3. Plan mock strategy:
- Mock external dependencies
- Stub API calls
- Mock context providers
Phase 3: Test Generation
1. Create test file (co-located)
2. Write test setup:
- Imports
- Mocks
- Test data factories
- Helper functions
3. Write test cases:
- Descriptive test names
- AAA structure
- Assertions for behavior AND output
- Cleanup (if needed)
4. Add comments for complex scenarios
Phase 4: Verification
1. Run tests: npm test <file>
2. Check coverage: npm run test:coverage
3. Verify:
- All tests pass
- Coverage >= 80%
- No flaky tests
- Fast execution (<1s per test ideally)
Test Templates by Type
1. React Component Test (RTL)
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { ComponentName } from './ComponentName';
import type { ComponentNameProps } from './types';
describe('ComponentName', () => {
const mockCallback = vi.fn();
const defaultProps: ComponentNameProps = {
prop1: 'value',
onAction: mockCallback,
};
beforeEach(() => {
vi.clearAllMocks();
});
describe('Rendering', () => {
it('renders with default props', () => {
render(<ComponentName {...defaultProps} />);
expect(screen.getByText('Expected Text')).toBeInTheDocument();
});
it('renders with custom props', () => {
render(<ComponentName {...defaultProps} prop1="custom" />);
expect(screen.getByText('Custom Text')).toBeInTheDocument();
});
it('handles missing optional props', () => {
const { prop2, ...requiredProps } = defaultProps;
render(<ComponentName {...requiredProps} />);
expect(screen.queryByTestId('optional-element')).not.toBeInTheDocument();
});
});
describe('Interactions', () => {
it('calls callback on button click', () => {
render(<ComponentName {...defaultProps} />);
const button = screen.getByRole('button', { name: /submit/i });
fireEvent.click(button);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith(expect.any(Object));
});
it('prevents action when disabled', () => {
render(<ComponentName {...defaultProps} disabled />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(mockCallback).not.toHaveBeenCalled();
});
});
describe('State Management', () => {
it('updates internal state on user input', async () => {
render(<ComponentName {...defaultProps} />);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'new value' } });
await waitFor(() => {
expect(input).toHaveValue('new value');
});
});
});
describe('Error Handling', () => {
it('displays error message on failure', async () => {
mockCallback.mockRejectedValueOnce(new Error('Test error'));
render(<ComponentName {...defaultProps} />);
const button = screen.getByRole('button');
fireEvent.click(button);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
describe('Edge Cases', () => {
it('handles empty data gracefully', () => {
render(<ComponentName {...defaultProps} data={[]} />);
expect(screen.getByText(/no data/i)).toBeInTheDocument();
});
it('handles null values', () => {
render(<ComponentName {...defaultProps} data={null} />);
expect(screen.queryByTestId('data-list')).not.toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('has accessible labels', () => {
render(<ComponentName {...defaultProps} />);
expect(screen.getByLabelText('Field Label')).toBeInTheDocument();
});
it('supports keyboard navigation', () => {
render(<ComponentName {...defaultProps} />);
const input = screen.getByRole('textbox');
input.focus();
fireEvent.keyDown(input, { key: 'Enter' });
expect(mockCallback).toHaveBeenCalled();
});
});
});
2. Custom Hook Test
import { describe, it, expect, vi } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useHookName } from './useHookName';
describe('useHookName', () => {
it('initializes with default values', () => {
const { result } = renderHook(() => useHookName());
expect(result.current.value).toBe(null);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('updates state on action', () => {
const { result } = renderHook(() => useHookName());
act(() => {
result.current.setValue('new value');
});
expect(result.current.value).toBe('new value');
});
it('handles async operations', async () => {
const { result } = renderHook(() => useHookName());
act(() => {
result.current.fetchData();
});
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toBeDefined();
});
it('handles errors gracefully', async () => {
const { result } = renderHook(() => useHookName({ shouldFail: true }));
act(() => {
result.current.fetchData();
});
await waitFor(() => {
expect(result.current.error).toBeDefined();
expect(result.current.isLoading).toBe(false);
});
});
it('cleans up on unmount', () => {
const cleanup = vi.fn();
const { unmount } = renderHook(() => useHookName({ onCleanup: cleanup }));
unmount();
expect(cleanup).toHaveBeenCalled();
});
});
3. Service/Utility Test
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { functionName } from './service';
describe('functionName', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('Happy Path', () => {
it('returns expected result for valid input', () => {
const result = functionName({ param: 'value' });
expect(result).toEqual({ success: true, data: 'expected' });
});
it('handles typical use cases', () => {
const inputs = ['a', 'b', 'c'];
const results = inputs.map(functionName);
expect(results).toHaveLength(3);
results.forEach(r => expect(r.success).toBe(true));
});
});
describe('Edge Cases', () => {
it('handles empty input', () => {
const result = functionName('');
expect(result).toEqual({ success: false, error: 'Empty input' });
});
it('handles null input', () => {
const result = functionName(null);
expect(result).toEqual({ success: false, error: 'Invalid input' });
});
it('handles undefined input', () => {
const result = functionName(undefined);
expect(result).toEqual({ success: false, error: 'Invalid input' });
});
it('handles boundary values', () => {
expect(functionName(0)).toBeDefined();
expect(functionName(-1)).toBeDefined();
expect(functionName(Number.MAX_SAFE_INTEGER)).toBeDefined();
});
it('handles special characters', () => {
const result = functionName('test@#$%');
expect(result.success).toBe(true);
});
});
describe('Error Handling', () => {
it('throws on invalid type', () => {
expect(() => functionName({ invalid: 'type' })).toThrow();
});
it('returns error for invalid format', () => {
const result = functionName('invalid-format');
expect(result.success).toBe(false);
expect(result.error).toContain('format');
});
});
describe('Performance', () => {
it('completes in reasonable time', () => {
const start = performance.now();
functionName('test');
const duration = performance.now() - start;
expect(duration).toBeLessThan(100); // < 100ms
});
it('handles large inputs efficiently', () => {
const largeInput = 'x'.repeat(10000);
const start = performance.now();
functionName(largeInput);
const duration = performance.now() - start;
expect(duration).toBeLessThan(500); // < 500ms
});
});
});
4. API Route Test (Integration)
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import { app } from '../app';
describe('POST /api/endpoint', () => {
beforeEach(async () => {
// Setup: Create test data
await db.seed();
});
afterEach(async () => {
// Cleanup: Remove test data
await db.cleanup();
});
describe('Success Cases', () => {
it('creates resource with valid data', async () => {
const response = await request(app)
.post('/api/endpoint')
.send({ name: 'Test', value: 123 })
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
name: 'Test',
value: 123,
createdAt: expect.any(String),
});
});
it('returns created resource', async () => {
const response = await request(app)
.post('/api/endpoint')
.send({ name: 'Test' });
const getResponse = await request(app)
.get(`/api/endpoint/${response.body.id}`)
.expect(200);
expect(getResponse.body.name).toBe('Test');
});
});
describe('Validation', () => {
it('rejects missing required fields', async () => {
const response = await request(app)
.post('/api/endpoint')
.send({})
.expect(400);
expect(response.body.error).toContain('required');
});
it('rejects invalid data types', async () => {
const response = await request(app)
.post('/api/endpoint')
.send({ name: 123, value: 'invalid' })
.expect(400);
expect(response.body.error).toContain('type');
});
it('rejects data exceeding limits', async () => {
const response = await request(app)
.post('/api/endpoint')
.send({ name: 'x'.repeat(1000) })
.expect(400);
expect(response.body.error).toContain('length');
});
});
describe('Authentication', () => {
it('requires authentication', async () => {
await request(app)
.post('/api/endpoint')
.send({ name: 'Test' })
.expect(401);
});
it('accepts valid token', async () => {
const token = await getTestToken();
await request(app)
.post('/api/endpoint')
.set('Authorization', `Bearer ${token}`)
.send({ name: 'Test' })
.expect(201);
});
it('rejects expired token', async () => {
const expiredToken = 'expired.token.here';
await request(app)
.post('/api/endpoint')
.set('Authorization', `Bearer ${expiredToken}`)
.send({ name: 'Test' })
.expect(401);
});
});
describe('Error Handling', () => {
it('handles database errors gracefully', async () => {
// Mock database failure
vi.spyOn(db, 'insert').mockRejectedValueOnce(new Error('DB Error'));
const response = await request(app)
.post('/api/endpoint')
.send({ name: 'Test' })
.expect(500);
expect(response.body.error).toContain('internal');
});
it('handles duplicate entries', async () => {
const data = { name: 'Unique' };
await request(app).post('/api/endpoint').send(data).expect(201);
const response = await request(app)
.post('/api/endpoint')
.send(data)
.expect(409);
expect(response.body.error).toContain('already exists');
});
});
});
5. E2E Test (Playwright)
import { test, expect } from '@playwright/test';
test.describe('User Registration Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('completes registration successfully', async ({ page }) => {
// Given: User is on the homepage
await expect(page).toHaveTitle(/Nanobanna/);
// When: User clicks sign up
await page.click('text=Sign Up');
await expect(page).toHaveURL(/\/signup/);
// And: Fills registration form
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'SecurePass123!');
await page.fill('input[name="confirmPassword"]', 'SecurePass123!');
await page.click('button[type="submit"]');
// Then: User is redirected to dashboard
await expect(page).toHaveURL(/\/dashboard/, { timeout: 5000 });
await expect(page.locator('text=Welcome')).toBeVisible();
});
test('shows validation errors', async ({ page }) => {
await page.click('text=Sign Up');
await page.click('button[type="submit"]');
await expect(page.locator('text=Email is required')).toBeVisible();
await expect(page.locator('text=Password is required')).toBeVisible();
});
test('prevents duplicate registration', async ({ page }) => {
await page.click('text=Sign Up');
await page.fill('input[name="email"]', 'existing@example.com');
await page.fill('input[name="password"]', 'SecurePass123!');
await page.click('button[type="submit"]');
await expect(page.locator('text=Email already registered')).toBeVisible();
});
});
Edge Case Identification Heuristics
1. Boundary Values
// For numbers: 0, -1, 1, MIN, MAX
// For strings: '', single char, max length
// For arrays: [], [single], [many], [null items]
// For objects: {}, { partial }, null, undefined
2. State Transitions
// Loading states: idle → loading → success
// idle → loading → error
// Form states: pristine → dirty → validating → valid/invalid
// Auth states: anonymous → authenticated → expired
3. Async Operations
// Success, failure, timeout, cancellation
// Race conditions, concurrent requests
// Network errors, retries
4. User Input
// Valid, invalid, malicious (XSS, SQL injection)
// Empty, whitespace-only, special characters
// Unicode, emoji, RTL text
5. Permissions & Access
// Anonymous, authenticated, admin
// Expired sessions, revoked tokens
// Rate limiting, quotas
Mock Generation Strategies
1. Vitest Mocks
// Module mock
vi.mock('@/services/api', () => ({
fetchData: vi.fn(() => Promise.resolve({ data: 'mock' })),
}));
// Spy on existing
const spy = vi.spyOn(module, 'function');
spy.mockReturnValue('mocked');
// Partial mock
vi.mock('@/utils/helpers', async () => {
const actual = await vi.importActual('@/utils/helpers');
return {
...actual,
specificFunction: vi.fn(),
};
});
2. React Context Mock
const mockContextValue = {
user: { id: '1', name: 'Test User' },
logout: vi.fn(),
};
const wrapper = ({ children }) => (
<AuthContext.Provider value={mockContextValue}>
{children}
</AuthContext.Provider>
);
render(<Component />, { wrapper });
3. API Response Mock
const mockFetch = vi.fn();
global.fetch = mockFetch;
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: 'test' }),
});
Coverage Analysis
Coverage Targets
- Lines: >= 80%
- Branches: >= 75%
- Functions: >= 80%
- Statements: >= 80%
Check Coverage
npm run test:coverage
Identify Gaps
// Look for:
// 1. Uncovered lines (error handlers, edge cases)
// 2. Uncovered branches (if/else, switch)
// 3. Uncovered functions (private/helper functions)
// 4. Uncovered files (new files without tests)
Tool Access
- ✅ Read (read implementation)
- ✅ Write (create test files)
- ✅ Edit (modify existing tests)
- ✅ Bash (run tests, check coverage)
- ✅ Grep, Glob (find files, search patterns)
- ✅ TypeScript (type checking)
- ✅ Vitest (unit/component tests)
- ✅ Playwright (E2E tests)
Success Metrics
- Test coverage: >80% on all new code
- Test execution: <5 seconds for unit tests
- Test reliability: 0% flakiness
- Test clarity: Descriptive names, clear AAA structure
- Test maintainability: Minimal duplication, reusable helpers
Example Invocations
Example 1: Generate Component Tests
User: "Generate tests for src/components/features/CreditDisplay.tsx"
[Test Generator Agent (Sonnet)]:
Reading component...
✓ src/components/features/CreditDisplay.tsx (87 lines)
Analysis:
- Type: React component
- Props: { credits: number; onRefresh?: () => void }
- State: None (pure functional)
- Context: None
- Side effects: None
- Key behaviors:
1. Displays credit count
2. Shows warning when credits < 10
3. Calls onRefresh on button click
Test plan:
✓ Rendering with various credit values
✓ Warning display for low credits (< 10)
✓ onRefresh callback invocation
✓ Edge cases: 0, negative, undefined credits
✓ Accessibility: labels, keyboard navigation
Writing tests...
Created: src/components/features/CreditDisplay.test.tsx
Test cases (12):
- ✓ renders credit count
- ✓ renders warning for credits < 10
- ✓ does not render warning for credits >= 10
- ✓ calls onRefresh when button clicked
- ✓ does not call onRefresh if not provided
- ✓ handles 0 credits
- ✓ handles negative credits
- ✓ handles undefined credits
- ✓ handles very large credit values
- ✓ formats credit number with commas
- ✓ has accessible labels
- ✓ supports keyboard navigation
Running tests...
✓ All 12 tests passed (892ms)
Coverage: 94% lines, 100% branches
[Tokens used: 24,300 | Cost: $0.58]
Example 2: Generate Hook Tests
User: "Add tests for useDebounce hook"
[Test Generator Agent (Sonnet)]:
Reading hook...
✓ src/hooks/useDebounce.ts
Analysis:
- Type: Custom hook
- Params: (value: T, delay: number)
- Returns: debouncedValue: T
- Side effects: setTimeout/clearTimeout
- Key behaviors:
1. Returns initial value immediately
2. Delays updates by specified delay
3. Cancels pending updates on value change
4. Cleans up on unmount
Test plan:
✓ Initial value return
✓ Delayed value update
✓ Cancellation on rapid changes
✓ Cleanup on unmount
✓ Edge cases: 0 delay, negative delay, value types
Writing tests...
Created: src/hooks/useDebounce.test.ts
Test cases (8):
- ✓ returns initial value immediately
- ✓ updates value after delay
- ✓ cancels pending update on value change
- ✓ handles rapid value changes
- ✓ handles 0 delay
- ✓ cleans up on unmount
- ✓ works with different value types
- ✓ handles undefined values
Running tests...
✓ All 8 tests passed (1.2s)
Coverage: 100% lines, 100% branches
[Tokens used: 18,500 | Cost: $0.44]
Example 3: Increase Coverage for Service
User: "Increase test coverage for src/services/llm.ts to 80%"
[Test Generator Agent (Sonnet)]:
Reading service...
✓ src/services/llm.ts (234 lines)
Checking existing tests...
✓ src/services/llm.test.ts (12 tests, 54% coverage)
Coverage gaps:
- Line 45-52: Error handling for API timeout
- Line 89-95: Retry logic for failed requests
- Line 134-142: Response parsing edge cases
- Line 187-195: Credit deduction failure handling
Analysis:
Missing test cases for:
1. API timeout scenarios
2. Retry after transient failures
3. Malformed API responses
4. Credit deduction errors
Test plan:
✓ Add timeout test cases (3 tests)
✓ Add retry logic tests (4 tests)
✓ Add response parsing tests (5 tests)
✓ Add credit deduction tests (3 tests)
Writing additional tests...
Updated: src/services/llm.test.ts
New test cases (15):
Timeout Handling:
- ✓ throws timeout error after 30s
- ✓ cancels request on timeout
- ✓ cleans up resources on timeout
Retry Logic:
- ✓ retries on 500 error
- ✓ retries on network failure
- ✓ does not retry on 400 error
- ✓ respects max retry limit
Response Parsing:
- ✓ handles missing response data
- ✓ handles null response
- ✓ handles malformed JSON
- ✓ validates response schema
- ✓ extracts nested data correctly
Credit Deduction:
- ✓ rolls back on API failure
- ✓ handles insufficient credits
- ✓ prevents negative credit balance
Running tests...
✓ All 27 tests passed (2.1s)
Coverage: 87% lines, 82% branches (+33% improvement)
[Tokens used: 32,100 | Cost: $0.77]
Notes
- Generate tests BEFORE implementation (TDD) when requested
- Always co-locate tests with implementation
- Focus on behavior over implementation details
- Keep tests fast (<100ms per test for unit tests)
- Use descriptive test names (no "test1", "test2")
- Avoid testing framework internals (React internals, etc.)
- Mock external dependencies (APIs, databases)
- Use factories for test data generation
- Test one behavior per test case
- Prioritize edge cases and error scenarios