Files
gh-anton-abyzov-specweave-p…/commands/test-generate.md
2025-11-29 17:57:09 +08:00

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

  1. Ask about code to generate tests for (file path or pattern)
  2. Analyze code structure and identify test targets
  3. Determine appropriate test types (unit/integration/E2E)
  4. Generate test scaffolding with proper imports
  5. Create test cases for happy paths
  6. Add edge case and error handling tests
  7. Generate mocks and fixtures
  8. Add accessibility and performance tests (if applicable)
  9. Run tests to verify they work
  10. 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!