Generate well-structured, maintainable test suites that provide confidence in code correctness. Follow the test pyramid approach: many unit tests, fewer integration tests, minimal E2E tests.
Test Generation Workflow
Step 1: Analyze the Code Under Test
- Read the source file(s) to understand functionality.
- Identify all public functions, methods, and exported interfaces.
- Map code paths — branches, loops, error handlers, early returns.
- Identify external dependencies — APIs, databases, file system, third-party services.
- Note any existing tests to avoid duplication and maintain consistency.
Step 2: Identify Test Cases
For each function or method, identify:
Happy Path Cases
- Standard inputs producing expected outputs.
- Common use cases as described in requirements or documentation.
Edge Cases
- Null/undefined/empty inputs: What happens with no data?
- Boundary values: Minimum, maximum, zero, negative numbers, empty strings, single-character strings.
- Type boundaries: Integer overflow, floating point precision, very long strings.
- Collection edges: Empty array, single element, very large collections.
- Concurrent access: Race conditions, simultaneous modifications.
Error Cases
- Invalid input types or formats.
- Missing required fields.
- External service failures (network errors, timeouts, 5xx responses).
- Permission/authorization failures.
- Resource not found scenarios.
- Data constraint violations (unique, foreign key, validation).
State Transitions
- Before/after states for mutations.
- Idempotency verification — calling twice produces the same result.
- Order-dependent behavior.
Step 3: Write Tests Using AAA Pattern
Every test follows Arrange, Act, Assert:
// Arrange — set up test data, mocks, and preconditions
// Act — execute the function or operation under test
// Assert — verify the outcome matches expectations
Test Structure Rules
- One assertion concept per test — a test should verify one behavior. Multiple
expectcalls are fine if they verify the same logical assertion. - Descriptive test names:
should [expected behavior] when [condition/context] - No test logic — no conditionals, loops, or try/catch in tests. If you need them, the test is too complex.
- Independent tests — no shared mutable state, no execution order dependencies.
- Fast tests — unit tests should run in milliseconds. Mock external dependencies.
Step 4: Apply Mocking Strategy
When to Mock
- External HTTP APIs and services.
- Database connections and queries.
- File system operations.
- Time-dependent operations (
Date.now(), timers). - Random number generation.
- Environment variables and configuration.
When NOT to Mock
- The code under test itself.
- Pure utility functions with no side effects.
- Value objects and data structures.
- Simple internal collaborators that are fast and deterministic.
Mock Quality Rules
- Mocks should match the real interface — use type-safe mocks when possible.
- Verify mock interactions only when the interaction IS the behavior (e.g., "sends an email").
- Prefer stubs (return values) over mocks (verify calls) for dependencies that provide data.
- Reset mocks between tests to prevent state leakage.
Step 5: Generate Tests by Type
Unit Tests
- Test individual functions and methods in isolation.
- Mock all external dependencies.
- Focus on logic, calculations, transformations, validations.
- Target: cover all code paths including error branches.
describe('calculateDiscount', () => {
it('should apply 10% discount for orders over $100', () => {
const order = createOrder({ subtotal: 150 });
const result = calculateDiscount(order);
expect(result).toBe(15);
});
it('should return 0 discount for orders under $100', () => {
const order = createOrder({ subtotal: 50 });
const result = calculateDiscount(order);
expect(result).toBe(0);
});
it('should throw when order subtotal is negative', () => {
const order = createOrder({ subtotal: -10 });
expect(() => calculateDiscount(order)).toThrow('Invalid subtotal');
});
});
Integration Tests
- Test interactions between modules or services.
- Use real implementations for internal dependencies, mock external boundaries.
- Verify data flows correctly through the system.
- Test database queries against a test database (not mocked).
describe('UserService.createUser', () => {
it('should create user and send welcome email', async () => {
const emailService = createMockEmailService();
const service = new UserService(testDatabase, emailService);
const user = await service.createUser({ name: 'Jane', email: 'jane@example.com' });
expect(user.id).toBeDefined();
const savedUser = await testDatabase.users.findById(user.id);
expect(savedUser.name).toBe('Jane');
expect(emailService.sendWelcome).toHaveBeenCalledWith('jane@example.com');
});
});
E2E Tests
- Test complete user workflows through the full stack.
- Use real services (or dedicated test instances).
- Focus on critical business paths — registration, checkout, core CRUD operations.
- Keep the number small — these are slow and expensive.
Step 6: Coverage Guidance
| Test Type | Coverage Target | Focus |
|---|---|---|
| Unit | 80%+ line coverage | Business logic, calculations, validations |
| Integration | Critical paths covered | Service interactions, data persistence |
| E2E | Core workflows covered | User-facing flows, happy paths |
Coverage is a guide, not a goal. 80% meaningful coverage is better than 100% coverage with trivial assertions. Focus on:
- Code with complex logic or many branches.
- Code that handles money, permissions, or sensitive data.
- Code that has broken before (regression tests).
Skip testing:
- Simple getters/setters with no logic.
- Framework-generated boilerplate.
- Configuration objects with no behavior.
Test File Organization
mobile/src/
services/
user-service.ts
user-service.test.ts # Unit tests colocated
mobile/tests/
integration/
user-service.integration.ts # Integration tests separate
e2e/
user-registration.e2e.ts # E2E tests separate
fixtures/
users.ts # Shared test data factories
helpers/
test-database.ts # Test infrastructure utilities
Web Frontend Tests (Vitest + React Testing Library)
Component Tests (Vite SPA + Next.js Client Components)
// web/src/components/OrderTable.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { OrderTable } from './OrderTable';
import { createQueryWrapper } from '../../tests/utils';
describe('OrderTable', () => {
it('should render order rows when orders are provided', () => {
// Arrange
const orders = [{ id: '1', status: 'pending', totalAmount: 100 }];
// Act
render(<OrderTable orders={orders} />, { wrapper: createQueryWrapper() });
// Assert
expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByText('pending')).toBeInTheDocument();
});
it('should show empty state when no orders exist', () => {
render(<OrderTable orders={[]} />, { wrapper: createQueryWrapper() });
expect(screen.getByText(/no orders/i)).toBeInTheDocument();
});
});
Query Priority (React Testing Library)
Use this order: getByRole > getByLabelText > getByText > getByTestId.
TanStack Query Hook Tests with MSW
import { renderHook, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { server } from '../../tests/msw-server';
import { useOrders } from '../orders';
import { createQueryWrapper } from '../../tests/utils';
describe('useOrders', () => {
it('should fetch orders from the API', async () => {
// Arrange
server.use(
http.get('*/api/v1/orders', () => HttpResponse.json([{ id: '1', status: 'pending' }])),
);
// Act
const { result } = renderHook(() => useOrders(), { wrapper: createQueryWrapper() });
// Assert
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toHaveLength(1);
});
});
Next.js Server Component Tests
Test Server Components as async functions — call and assert on returned JSX:
import { describe, it, expect, vi } from 'vitest';
import OrdersPage from '@/app/orders/page';
vi.mock('@/api/client', () => ({
railsApi: { get: vi.fn().mockResolvedValue([{ id: '1', status: 'pending' }]) },
}));
describe('OrdersPage (Server Component)', () => {
it('should fetch and render orders', async () => {
// Act — call as async function
const page = await OrdersPage();
// Assert — Server Component returns JSX
expect(page).toBeTruthy();
});
});
Next.js Server Action Tests
Test server actions as async functions with FormData:
import { describe, it, expect, vi } from 'vitest';
import { createOrder } from '@/actions/orders';
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }));
vi.mock('next/navigation', () => ({ redirect: vi.fn() }));
describe('createOrder', () => {
it('should return validation errors for invalid input', async () => {
// Arrange
const formData = new FormData();
// Act
const result = await createOrder(null, formData);
// Assert
expect(result?.errors?.customerName).toBeDefined();
});
});
Test Wrapper Utility
// tests/utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
export function createQueryWrapper() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>{children}</BrowserRouter>
</QueryClientProvider>
);
}
Output
When generating tests, provide:
- The complete test file(s) with all test cases.
- Any required test fixtures or factories.
- A brief summary of what is covered and any known gaps.
- Setup instructions if new test infrastructure is needed.