testgen.md 21 KB


description: "Generate tests with expert routing, framework detection, and auto-TodoWrite. Creates unit, integration, or E2E tests following project conventions."

TestGen - AI Test Generation

Generate comprehensive tests for your code with automatic framework detection, expert agent routing, and project convention matching. Routes to specialized experts (python-expert, react-expert, cypress-expert) for domain-specific test patterns.

Arguments

$ARGUMENTS

  • <file>: Generate tests for specific file
  • <file>:<function>: Generate tests for specific function/method
  • <directory>: Generate tests for all files in directory
  • --type <unit|integration|e2e|component>: Specify test type
  • --framework <jest|vitest|pytest|...>: Override detected framework
  • --focus <happy|edge|error|all>: Focus on specific test cases
  • --depth <quick|normal|thorough>: Generation depth
  • --stubs: Generate empty test stubs only

Architecture

/testgen <target> [--type] [--focus] [--depth]
    │
    ├─→ Step 1: Analyze Target
    │     ├─ File exists? → Read and parse
    │     ├─ Function specified? → Extract signature
    │     ├─ Directory? → List source files
    │     └─ Find existing tests (avoid duplicates)
    │
    ├─→ Step 2: Detect Framework (parallel)
    │     ├─ package.json → jest/vitest/mocha/cypress/playwright
    │     ├─ pyproject.toml → pytest/unittest
    │     ├─ composer.json → phpunit/pest
    │     ├─ Check existing test patterns
    │     └─ Fallback: infer from file extension
    │
    ├─→ Step 3: Load Project Standards
    │     ├─ AGENTS.md, CLAUDE.md conventions
    │     ├─ Existing test file structure
    │     ├─ Import styles and assertion library
    │     └─ Naming conventions (*.test.ts vs *.spec.ts)
    │
    ├─→ Step 4: Route to Expert Agent
    │     ├─ .ts → typescript-expert
    │     ├─ .tsx/.jsx → react-expert
    │     ├─ .vue → vue-expert
    │     ├─ .py → python-expert
    │     ├─ .php → laravel-expert
    │     ├─ E2E tests → cypress-expert
    │     └─ Multi-file → parallel expert dispatch
    │
    ├─→ Step 5: Generate Tests
    │     ├─ Create test file in correct location
    │     ├─ Follow detected conventions
    │     ├─ Include: happy path, edge cases, error handling
    │     └─ Add proper mocking for dependencies
    │
    └─→ Step 6: Integration
          ├─ Auto-create TodoWrite for generated tests
          ├─ Suggest: run tests to verify
          └─ Link to /plan --save for tracking

Execution Steps

Step 1: Analyze Target

# Check if target exists
test -f "$TARGET" && echo "FILE" || test -d "$TARGET" && echo "DIRECTORY"

# For function-specific: parse the file
# /testgen src/auth.ts:validateToken → extract validateToken signature

Extract function signature:

# Use ast-grep if available
command -v ast-grep >/dev/null 2>&1 && ast-grep -p "function $FUNCTION_NAME" "$FILE"

# Fallback to ripgrep
rg "(?:function|const|def|public|private)\s+$FUNCTION_NAME" "$FILE" -A 10

Check for existing tests:

# Find related test files
fd -e test.ts -e spec.ts -e test.js -e spec.js | rg "$BASENAME"

# Python
fd "test_*.py" | rg "$BASENAME"

Step 2: Detect Framework

JavaScript/TypeScript:

# Check package.json devDependencies
cat package.json 2>/dev/null | jq -r '.devDependencies | keys[]' | grep -E 'jest|vitest|mocha|cypress|playwright|@testing-library'

Python:

# Check pyproject.toml or requirements
grep -E "pytest|unittest|nose" pyproject.toml setup.py requirements*.txt 2>/dev/null

PHP:

# Check composer.json
cat composer.json 2>/dev/null | jq -r '.["require-dev"] | keys[]' | grep -E 'phpunit|pest|codeception'

Detect test patterns:

# Find existing test files to match conventions
fd -e test.ts -e spec.ts -e test.tsx -e spec.tsx | head -3
fd "test_*.py" tests/ | head -3

Step 3: Load Project Standards

Check for conventions:

# Claude Code conventions
cat AGENTS.md 2>/dev/null | head -50
cat CLAUDE.md 2>/dev/null | head -50

# Test config files
cat jest.config.* vitest.config.* pytest.ini pyproject.toml 2>/dev/null | head -30

Determine test location convention:

# JavaScript conventions (detect which is used)
src/utils/helper.ts → src/utils/__tests__/helper.test.ts  # __tests__ folder
                    → src/utils/helper.test.ts            # co-located
                    → tests/utils/helper.test.ts          # separate tests/

# Python conventions
app/utils/helper.py → tests/test_helper.py               # tests/ folder
                    → tests/utils/test_helper.py         # mirror structure
                    → app/utils/test_helper.py           # co-located

# PHP conventions
app/Services/UserService.php → tests/Unit/Services/UserServiceTest.php
                             → tests/Feature/UserServiceTest.php

Step 4: Route to Expert Agent

File Pattern Primary Expert Secondary
*.ts typescript-expert -
*.tsx, *.jsx react-expert typescript-expert
*.vue vue-expert typescript-expert
*.py python-expert -
*.php laravel-expert -
*.cy.ts, cypress/* cypress-expert -
*.spec.ts (Playwright) - typescript-expert
*.sh, *.bash bash-expert -

Invoke via Task tool:

Task tool with subagent_type: "[detected]-expert"
Prompt includes:
  - Source file content
  - Function signatures to test
  - Detected framework and conventions
  - Requested test type and focus
  - Project conventions from AGENTS.md

Step 5: Generate Tests

The expert produces tests following this structure:

Include test categories based on --focus:

Focus What to Generate
happy Normal input, expected output
edge Boundary values, empty inputs, nulls
error Invalid inputs, exceptions, error handling
all All of the above (default)

Depth levels:

Depth Coverage
quick Happy path only, 1-2 tests per function
normal Happy + common edge cases (default)
thorough Comprehensive: all paths, mocking, async

Step 6: Integration

Auto-create TodoWrite:

TodoWrite:
  - content: "Run generated tests for src/auth.ts"
    status: "pending"
    activeForm: "Running generated tests for auth.ts"

Suggest next steps:

Tests generated: src/auth.test.ts

Next steps:
1. Run tests: npm test src/auth.test.ts
2. Review and refine edge cases
3. Use /plan --save to track test coverage goals

Expert Routing Details

TypeScript/JavaScript → typescript-expert

Generates tests with:

  • Proper type imports
  • Generic type handling
  • Async/await patterns
  • Mock typing

React/JSX → react-expert

Generates tests with:

  • React Testing Library patterns
  • Component rendering tests
  • Hook testing (renderHook)
  • Event simulation
  • Accessibility queries (getByRole)

Vue → vue-expert

Generates tests with:

  • Vue Test Utils patterns
  • Composition API testing
  • Pinia store mocking
  • Component mounting

Python → python-expert

Generates tests with:

  • pytest fixtures
  • Parametrized tests
  • Mock/patch patterns
  • Async test handling
  • Type hint verification

PHP/Laravel → laravel-expert

Generates tests with:

  • PHPUnit/Pest patterns
  • Database transactions
  • Factory usage
  • HTTP testing
  • Mocking facades

E2E → cypress-expert

Generates tests with:

  • Page object patterns
  • Custom commands
  • Network stubbing
  • Visual testing
  • CI configuration

Framework-Specific Output

Jest/Vitest (TypeScript)

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { validateToken, TokenError } from '../auth';

describe('validateToken', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('happy path', () => {
    it('should return true for valid JWT token', () => {
      const token = 'eyJhbGciOiJIUzI1NiIs...';
      expect(validateToken(token)).toBe(true);
    });

    it('should decode payload correctly', () => {
      const token = createTestToken({ userId: 123 });
      const result = validateToken(token);
      expect(result.payload.userId).toBe(123);
    });
  });

  describe('edge cases', () => {
    it('should handle empty string', () => {
      expect(validateToken('')).toBe(false);
    });

    it('should handle malformed token', () => {
      expect(validateToken('not.a.token')).toBe(false);
    });

    it('should handle expired token', () => {
      const expiredToken = createTestToken({ exp: Date.now() - 1000 });
      expect(validateToken(expiredToken)).toBe(false);
    });
  });

  describe('error handling', () => {
    it('should throw TokenError for null input', () => {
      expect(() => validateToken(null)).toThrow(TokenError);
    });

    it('should throw with descriptive message', () => {
      expect(() => validateToken(null)).toThrow('Token cannot be null');
    });
  });
});

React Testing Library

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from '../LoginForm';

describe('LoginForm', () => {
  const mockOnSubmit = vi.fn();

  beforeEach(() => {
    mockOnSubmit.mockClear();
  });

  it('renders email and password fields', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    expect(screen.getByRole('textbox', { name: /email/i })).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
  });

  it('submits form with credentials', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.type(screen.getByRole('textbox', { name: /email/i }), 'test@example.com');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(mockOnSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123',
    });
  });

  it('shows validation error for invalid email', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.type(screen.getByRole('textbox', { name: /email/i }), 'invalid');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(await screen.findByText(/invalid email/i)).toBeInTheDocument();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('disables submit button while loading', () => {
    render(<LoginForm onSubmit={mockOnSubmit} isLoading />);

    expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
  });
});

pytest (Python)

import pytest
from unittest.mock import Mock, patch, AsyncMock
from app.auth import validate_token, TokenError

class TestValidateToken:
    """Tests for validate_token function."""

    def test_valid_token_returns_true(self):
        """Should return True for valid JWT token."""
        token = "eyJhbGciOiJIUzI1NiIs..."
        assert validate_token(token) is True

    def test_decodes_payload_correctly(self, valid_token):
        """Should decode payload with correct user ID."""
        result = validate_token(valid_token)
        assert result.payload["userId"] == 123

    @pytest.mark.parametrize("invalid_input", [
        "",
        "not.a.token",
        "a.b",
        None,
    ])
    def test_rejects_invalid_tokens(self, invalid_input):
        """Should return False for invalid token formats."""
        assert validate_token(invalid_input) is False

    def test_rejects_expired_token(self, expired_token):
        """Should return False for expired tokens."""
        assert validate_token(expired_token) is False

    def test_raises_token_error_for_null(self):
        """Should raise TokenError with descriptive message."""
        with pytest.raises(TokenError, match="Token cannot be null"):
            validate_token(None)

    @pytest.fixture
    def valid_token(self):
        """Create a valid test token."""
        return create_test_token({"userId": 123})

    @pytest.fixture
    def expired_token(self):
        """Create an expired test token."""
        return create_test_token({"exp": time.time() - 1000})


class TestValidateTokenAsync:
    """Tests for async token validation."""

    @pytest.mark.asyncio
    async def test_async_validation(self):
        """Should validate token asynchronously."""
        token = create_test_token({"userId": 456})
        result = await validate_token_async(token)
        assert result.valid is True

    @pytest.mark.asyncio
    async def test_handles_network_timeout(self):
        """Should handle network timeout gracefully."""
        with patch("app.auth.fetch_public_key", new_callable=AsyncMock) as mock:
            mock.side_effect = TimeoutError()

            with pytest.raises(TokenError, match="Validation timeout"):
                await validate_token_async("token")

PHPUnit (PHP)

<?php

namespace Tests\Unit\Services;

use PHPUnit\Framework\TestCase;
use App\Services\AuthService;
use App\Exceptions\TokenException;
use Mockery;

class AuthServiceTest extends TestCase
{
    private AuthService $service;

    protected function setUp(): void
    {
        parent::setUp();
        $this->service = new AuthService();
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }

    /** @test */
    public function it_validates_correct_token(): void
    {
        $token = $this->createValidToken(['user_id' => 123]);

        $result = $this->service->validateToken($token);

        $this->assertTrue($result);
    }

    /** @test */
    public function it_rejects_expired_token(): void
    {
        $token = $this->createExpiredToken();

        $result = $this->service->validateToken($token);

        $this->assertFalse($result);
    }

    /** @test */
    public function it_throws_for_null_token(): void
    {
        $this->expectException(TokenException::class);
        $this->expectExceptionMessage('Token cannot be null');

        $this->service->validateToken(null);
    }

    /**
     * @test
     * @dataProvider invalidTokenProvider
     */
    public function it_rejects_invalid_tokens(string $invalidToken): void
    {
        $result = $this->service->validateToken($invalidToken);

        $this->assertFalse($result);
    }

    public static function invalidTokenProvider(): array
    {
        return [
            'empty string' => [''],
            'malformed' => ['not.a.token'],
            'missing parts' => ['a.b'],
        ];
    }
}

Pest (PHP)

<?php

use App\Services\AuthService;
use App\Exceptions\TokenException;

describe('AuthService', function () {
    beforeEach(function () {
        $this->service = new AuthService();
    });

    describe('validateToken', function () {
        it('validates correct token', function () {
            $token = createValidToken(['user_id' => 123]);

            expect($this->service->validateToken($token))->toBeTrue();
        });

        it('rejects expired token', function () {
            $token = createExpiredToken();

            expect($this->service->validateToken($token))->toBeFalse();
        });

        it('throws for null token', function () {
            $this->service->validateToken(null);
        })->throws(TokenException::class, 'Token cannot be null');

        it('rejects invalid tokens', function (string $invalidToken) {
            expect($this->service->validateToken($invalidToken))->toBeFalse();
        })->with([
            'empty string' => '',
            'malformed' => 'not.a.token',
            'missing parts' => 'a.b',
        ]);
    });
});

Cypress (E2E)

describe('Login Flow', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('should login with valid credentials', () => {
    cy.get('[data-cy=email]').type('user@example.com');
    cy.get('[data-cy=password]').type('password123');
    cy.get('[data-cy=submit]').click();

    cy.url().should('include', '/dashboard');
    cy.get('[data-cy=welcome]').should('contain', 'Welcome');
  });

  it('should show error with invalid credentials', () => {
    cy.intercept('POST', '/api/login', {
      statusCode: 401,
      body: { error: 'Invalid credentials' },
    }).as('loginRequest');

    cy.get('[data-cy=email]').type('user@example.com');
    cy.get('[data-cy=password]').type('wrong');
    cy.get('[data-cy=submit]').click();

    cy.wait('@loginRequest');
    cy.get('[data-cy=error]').should('be.visible');
    cy.url().should('include', '/login');
  });

  it('should persist session after reload', () => {
    cy.login('user@example.com', 'password123');
    cy.visit('/dashboard');
    cy.reload();

    cy.get('[data-cy=welcome]').should('be.visible');
  });
});

Cypress (Component)

import LoginForm from './LoginForm.vue';

describe('LoginForm Component', () => {
  it('renders login form', () => {
    cy.mount(LoginForm);

    cy.get('[data-cy=email]').should('exist');
    cy.get('[data-cy=password]').should('exist');
    cy.get('[data-cy=submit]').should('contain', 'Login');
  });

  it('emits submit event with credentials', () => {
    const onSubmitSpy = cy.spy().as('submitSpy');
    cy.mount(LoginForm, { props: { onSubmit: onSubmitSpy } });

    cy.get('[data-cy=email]').type('user@example.com');
    cy.get('[data-cy=password]').type('password123');
    cy.get('[data-cy=submit]').click();

    cy.get('@submitSpy').should('have.been.calledWith', {
      email: 'user@example.com',
      password: 'password123',
    });
  });

  it('validates email format', () => {
    cy.mount(LoginForm);

    cy.get('[data-cy=email]').type('invalid-email');
    cy.get('[data-cy=submit]').click();

    cy.get('[data-cy=email-error]').should('contain', 'Invalid email');
  });
});

Usage Examples

# Generate tests for a file
/testgen src/utils/auth.ts

# Generate tests for specific function
/testgen src/utils/auth.ts:validateToken

# Generate tests for directory
/testgen src/services/

# Specify test type
/testgen src/api/users.ts --type integration

# Override framework detection
/testgen src/helpers.js --framework jest

# Focus on edge cases only
/testgen src/parser.ts --focus edge

# Quick generation (happy path only)
/testgen src/utils.ts --depth quick

# Thorough generation (all cases + mocking)
/testgen src/complex-service.ts --depth thorough

# Generate test stubs only (no implementation)
/testgen src/new-feature.ts --stubs

# Generate E2E tests
/testgen src/pages/Login.tsx --type e2e

# Generate component tests
/testgen src/components/Button.vue --type component

Focus Modes

Mode What's Generated Use When
--focus happy Normal inputs, expected outputs Quick smoke tests
--focus edge Boundaries, empty, null, limits Hardening existing code
--focus error Invalid inputs, exceptions Error handling coverage
--focus all Everything (default) New code, full coverage

Depth Modes

Mode Coverage Output Size
--depth quick Happy path, 1-2 tests/function Minimal
--depth normal Happy + common edges (default) Moderate
--depth thorough All paths, mocking, async, types Comprehensive

Smart Features

Dependency Detection

Automatically identifies and mocks:

  • External API calls (fetch, axios, httpx)
  • Database operations (queries, transactions)
  • File system operations
  • Environment variables
  • Third-party services

Test Location Intelligence

Detects project convention:

# Scans existing tests to match pattern
fd -e test.ts -e spec.ts | head -5

# Matches: __tests__/, co-located, or tests/

Import Style Matching

Matches existing test imports:

// Detects: vitest vs jest vs mocha
// Detects: @testing-library vs enzyme
// Detects: expect() style vs assert

CLI Tool Integration

Tool Purpose Fallback
jq Parse package.json Read tool
rg Find existing tests Grep tool
ast-grep Parse function signatures ripgrep patterns
fd Find test files Glob tool

Graceful degradation:

command -v jq >/dev/null 2>&1 && cat package.json | jq '.devDependencies' || cat package.json

Integration

Command Relationship
/review Review generated tests before committing
/explain Understand complex code before testing
/plan --save Track test coverage goals
/testgen This command

Notes

  • Generated tests are starting points - refine as needed
  • Review mocks for accuracy and completeness
  • Expert routing improves framework-specific patterns
  • Use --stubs when you prefer to write test logic yourself
  • Always run generated tests to verify they pass
  • Consider /review on generated tests before committing