Fair Supply LogoFair Supply - Docs

Testing Guide

Best practices for testing in the platform monorepo.

Commands

pnpm test                          # All tests
pnpm test:coverage                 # With coverage
pnpx vitest path/to/file.test.ts   # Specific file
pnpx vitest --watch                # Watch mode

Tools

ToolPurpose
VitestUnit tests
React Testing LibraryComponent tests
PlaywrightEnd-to-end tests

Test File Location

Co-locate test files with the code they test:

packages/core/src/
├── application/
│   └── use-cases/
│       └── transaction/
│           ├── create-transaction.ts
│           └── create-transaction.test.ts
├── infrastructure/
│   └── repositories/
│       ├── transaction-repository.ts
│       └── transaction-repository.test.ts

Test Pattern

import { describe, it, expect, vi, beforeEach } from 'vitest';

vi.mock('@repo/core/infrastructure/repositories/engagement-repository');

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

  it('creates engagement with valid input', async () => {
    // Arrange
    vi.mocked(EngagementRepository.create).mockResolvedValue(mockEngagement);

    // Act
    const result = await new CreateEngagement().execute(input);

    // Assert
    expect(result.engagement).toEqual(mockEngagement);
  });

  it('throws NotFoundError when org not found', async () => {
    vi.mocked(OrgRepository.findById).mockResolvedValue(null);

    await expect(useCase.execute(input)).rejects.toThrow(NotFoundError);
  });
});

What to Test

Test TypeExamples
User interactionsClick handlers, form submissions, keyboard navigation
Conditional renderingLoading states, error states, empty states
Data transformationsFormatting functions, filters, calculations
AccessibilityARIA attributes, keyboard support, screen reader text

What NOT to Test

  • Implementation details - Avoid testing internal state, private methods, or component internals
  • Third-party libraries - Trust that React Query, Zustand, etc. work correctly
  • Trivial code - Simple pass-through components or obvious logic don't need tests
  • Over-mocking - Over-mocking leads to tests that pass but don't catch real bugs
  • Test IDs first - Prefer accessible queries: getByRole, getByLabelText, getByText

Snapshot Tests

Use snapshots only for:

  • Serialized data structures - API responses, configuration objects, GraphQL queries
  • Generated output - Email templates, exported data formats, structured logs

Do not use snapshots for:

  • UI components (too brittle, changes are hard to review meaningfully)
  • Large objects (diffs become unreadable)

Diagnosing Failures

CauseSymptomFix
Mock not set up"undefined" errorsAdd vi.mock()
Missing awaitPromise comparison failsAdd await
State pollutionIntermittent failuresAdd beforeEach cleanup
Type mismatchMock doesn't matchUpdate mock shape
Logic changedAssertion failsUpdate test expectation

Fixing Tests

// Implementation changed - update expectation
expect(result.status).toBe('draft');  // was 'pending'

// New field added - update mock
vi.mocked(Repo.findById).mockResolvedValue({
  id: '123',
  name: 'Test',
  createdAt: new Date(),  // new field
});

// New validation - add test
it('throws ValidationError for invalid email', async () => {
  await expect(useCase.execute({ email: 'bad' })).rejects.toThrow(ValidationError);
});

Code Coverage

Thresholds by Package

Package TypeThresholdRationale
packages/core80%+Critical business logic
packages/luz60-70%UI components are harder to unit test
apps/web50-60%Presentation layer has less testable logic

What to Measure

Focus coverage on:

  • Use cases (packages/core/src/application/)
  • Repositories (packages/core/src/infrastructure/)
  • Utility functions and transformations

Exclude from coverage:

  • Generated types (GraphQL codegen)
  • Barrel files (index.ts re-exports)
  • Configuration files
  • Third-party wrapper code

Configuration Example

// packages/core/vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.test.ts', 'src/**/*.stories.tsx', 'src/index.ts'],
    },
  },
});

Best Practices

  1. Arrange-Act-Assert - clear test structure
  2. Descriptive names - it('returns null when not found')
  3. One assertion per test - focused tests
  4. Mock only dependencies - not implementation details
  5. Clear mocks in beforeEach - prevent pollution

Key Principle

Coverage is a tool, not a target. High coverage on trivial code is less valuable than moderate coverage on critical business logic. Focus testing effort on:

  1. Use cases with business rules
  2. Data transformations
  3. Error handling paths
  4. Edge cases in critical flows
TypeLocation
Vitest Configvitest.config.ts
Core Testspackages/core/src/**/*.test.ts
E2E Testsapps/web/e2e/