# Test Generator Agent

> Test Generator Agent

- Skill: `verridian-ai/test-generator-agent` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add verridian-ai/test-generator-agent`
- Raw SKILL.md: https://api.skillmd.com/api/skills/verridian-ai/test-generator-agent/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: Verridian-ai (https://skillmd.com/u/verridian-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/verridian-ai/test-generator-agent

---

# 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

1. **MUST read the component/function before writing tests**
2. **MUST follow shared_contract.md testing standards**
3. **MUST achieve minimum 80% coverage**
4. **MUST co-locate tests with implementation**
5. **MUST use AAA or Given-When-Then structure**
6. **MUST include edge cases and error scenarios**
7. **MUST use appropriate test types (unit/integration/e2e)**
8. **MUST generate realistic test data**

---

## Model Configuration

```json
{
  "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)

```typescript
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

```typescript
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

```typescript
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)

```typescript
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)

```typescript
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
```typescript
// 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
```typescript
// Loading states: idle → loading → success
//                 idle → loading → error
// Form states: pristine → dirty → validating → valid/invalid
// Auth states: anonymous → authenticated → expired
```

### 3. Async Operations
```typescript
// Success, failure, timeout, cancellation
// Race conditions, concurrent requests
// Network errors, retries
```

### 4. User Input
```typescript
// Valid, invalid, malicious (XSS, SQL injection)
// Empty, whitespace-only, special characters
// Unicode, emoji, RTL text
```

### 5. Permissions & Access
```typescript
// Anonymous, authenticated, admin
// Expired sessions, revoked tokens
// Rate limiting, quotas
```

---

## Mock Generation Strategies

### 1. Vitest Mocks
```typescript
// 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
```typescript
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
```typescript
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
```bash
npm run test:coverage
```

### Identify Gaps
```typescript
// 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

