Disciplines · Reference

Testing Strategy

The Oshun monorepo uses a layered testing approach with Vitest for unit and integration tests, Playwright for E2E tests, and k6 for performance tests.

7sections2 minread

On this page

Overview#

The Oshun monorepo uses a layered testing approach with Vitest for unit and integration tests, Playwright for E2E tests, and k6 for performance tests.


Test Framework: Vitest#

Configuration Hierarchy#

text
testing/
├── vitest.config.base.ts    # Shared base configuration
├── vitest.workspace.ts       # Multi-project workspace definition
├── setup/
│   ├── vitest.setup.ts       # Global test setup
│   └── vitest.jest-compat.ts # Jest compatibility layer
└── ...

Each project extends the base configuration with minimal overrides:

typescript
// libs/{domain}/{lib}/vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config';
import baseConfig from '../../../testing/vitest.config.base';

export default mergeConfig(
  baseConfig,
  defineConfig({
    test: {
      // Project-specific overrides only
    },
  })
);

Base Configuration#

Global settings:

  • Environment: node
  • Globals: enabled (Jest-compatible describe, it, expect)
  • ESBuild target: node20

Test file patterns:

text
src/**/*.{test,spec}.{ts,tsx}
__tests__/**/*.{test,spec}.{ts,tsx}
tests/**/*.{test,spec}.{ts,tsx}

Exclusions: node_modules, dist, build, .nx, E2E tests, integration tests, performance tests

Timeouts:

  • Test timeout: 30 seconds
  • Hook timeout: 30 seconds
  • Teardown timeout: 10 seconds

Thread pool:

  • CI: Maximum 4 isolated threads
  • Development: Unlimited threads

Retry policy (CI only):

  • Retry count: 2 attempts
  • Bail after: 5 failures

Coverage#

Provider: v8

Thresholds (enforced in CI):

Metric Minimum
Branches 80%
Functions 80%
Lines 80%
Statements 80%

Reporters: text, text-summary, json, html, lcov, cobertura

Coverage reports are generated in each project's coverage/ directory.

Path Aliases#

The base configuration maps @oshun/* and domain-specific imports to source directories so tests resolve correctly without building first:

typescript
// Shared libraries
'@oshun/types' → 'libs/shared/types/src'
'@oshun/errors' → 'libs/shared/errors/src'
'@oshun/database' → 'libs/shared/database/src'
// ... (all @oshun/* libraries)

// Domain contracts
'@oshun/contracts/{domain}' → 'libs/contracts/{domain}/src'

// Domain clients
'@oshun/clients/{domain}' → 'libs/shared/clients/{domain}/src'

Test Types#

Unit Tests#

Run with pnpm nx test <project>:

bash
# Test a specific library
pnpm nx test @oshun/database

# Test with coverage
pnpm nx test @oshun/database --coverage

# Test all affected projects
pnpm nx affected --target=test

Unit tests mock external dependencies and focus on individual module behavior.

Integration Tests#

Configuration: testing/vitest.config.integration.ts

Integration tests verify interactions between modules and with real databases. They typically require Docker services to be running:

bash
# Start infrastructure first
docker compose -f docker/docker-compose.dev.yml up -d

# Run integration tests
pnpm nx test:integration <project>

E2E Tests#

Framework: Playwright

Configuration: testing/e2e/

E2E tests validate complete user journeys through web applications:

bash
# Run E2E tests
pnpm nx e2e <project>

# Run with headed browser
pnpm nx e2e <project> --headed

The CI pipeline shards E2E tests across 3 workers for faster execution.

Performance Tests#

Framework: k6

Configuration: testing/performance/

bash
# Run performance benchmarks
pnpm nx perf <project>

The benchmarks.yml workflow runs k6 tests weekly with smoke, load, and stress modes.


Testing Specialized Areas#

Mobile Tests#

Configuration: testing/mobile/

Mobile test utilities for React Native apps (Tara, Veritas, Psyche).

Security Tests#

Configuration: testing/security/

Security testing utilities including vulnerability scanning and input validation.

Chaos Tests#

Configuration: testing/chaos/

Chaos engineering tests to verify resilience and graceful degradation.

Boundary Tests#

Configuration: testing/boundary/

Boundary testing for edge cases and limit validation.


Running Tests#

Common Commands#

bash
# Test a specific project
pnpm nx test @oshun/database

# Test all projects
pnpm test

# Test affected projects only
pnpm nx affected --target=test

# Test with watch mode
pnpm nx test @oshun/database --watch

# Test with coverage
pnpm nx test @oshun/database --coverage

# Run a specific test file
pnpm nx test @oshun/database -- --testPathPattern="sql.spec"

CI Output#

In CI, test results are output in multiple formats:

  • test-results/vitest-results.json - Machine-readable results
  • test-results/junit.xml - JUnit XML for CI integration
  • test-results/html/ - HTML report for visual inspection

Writing Tests#

Conventions#

  1. File naming: *.spec.ts or *.test.ts alongside the source file
  2. Describe blocks: Match the module or function being tested
  3. Test names: Start with "should" and describe expected behavior
  4. Arrange-Act-Assert: Structure each test clearly

Example#

typescript
import { describe, it, expect, vi } from 'vitest';
import { myFunction } from './my-module';

describe('myFunction', () => {
  it('should return the expected result', () => {
    const result = myFunction('input');
    expect(result).toBe('expected');
  });

  it('should throw on invalid input', () => {
    expect(() => myFunction('')).toThrow();
  });
});

Common Patterns#

  • Use vi.mock() for module mocking
  • Use vi.fn() for function spies
  • Use toEqual() for deep equality (not toBe() for objects)
  • Use toContainEqual() for array membership with deep equality
  • Use toBeCloseTo() for floating point comparisons
  • Prefix unused parameters with _ to satisfy @typescript-eslint/no-unused-vars

Further Reading#