Skip to main content

Testing

Refract uses two test runners: Jest for the backend (and tool packages) and Vitest for the frontend. They share a similar API, but they’re configured and invoked separately. Here’s everything you need to get comfortable writing and running tests.

Backend: Jest

The backend uses Jest with ts-jest — which means tests are written in TypeScript and compiled on the fly, no separate build step needed.

Config (apps/backend/jest.config.json)

{
  "maxWorkers": "50%",
  "collectCoverage": true,
  "coverageProvider": "v8",
  "coverageDirectory": "../reports",
  "coverageThreshold": {
    "global": {
      "statements": 80,
      "branches": 80,
      "functions": 70
    }
  },
  "transform": { "^.+\\.(ts|tsx)$": "ts-jest" },
  "testRegex": "/__tests__/.*\\.(spec|test)\\.ts$",
  "globalSetup": "<rootDir>/src/tools/rds/sequelize/setup.ts",
  "globalTeardown": "<rootDir>/src/tools/rds/sequelize/tearDown.ts",
  "clearMocks": true,
  "moduleNameMapper": { "^@/(.*)$": "<rootDir>/src/$1" }
}
A few things worth understanding here: Coverage thresholds — the suite will fail if statements or branches drop below 80%, or functions below 70%. This is enforced both locally and in CI. If you’re adding code without tests you’ll hit this wall pretty quickly. globalSetup / globalTeardown — before any tests run, Jest calls src/tools/rds/sequelize/setup.ts which runs your migrations against the test database. After all tests finish, tearDown.ts cleans up. This means your tests always have a freshly migrated schema available, without any test managing that setup itself. clearMocks: true — Jest automatically resets all mock state between tests. You don’t need to call jest.clearAllMocks() in afterEach. moduleNameMapper — the @/ path alias resolves to src/. So import { foo } from '@/utilities/foo' works in tests the same way it does in production code.

Running backend tests

# Run the full suite
make test module=backend

# Run a single spec file
make test module=backend path=src/tools/queue/__tests__/loader.spec.ts

# Run tests matching a name pattern
make test module=backend path=src/tools/queue/__tests__/loader.spec.ts -- --testNamePattern="SQS"

Coverage reports

Coverage output lands in apps/reports/ after each run:
  • backend-coverage.json — machine-readable coverage data (used by the CI coverage diff action)
  • backend.xml — JUnit XML (used by some CI reporting tools)
In CI, the integration.yml workflow posts a coverage diff comment on the PR showing you exactly which lines your changes covered or uncovered.

Frontend: Vitest

The frontend uses Vitest — it’s configured inside apps/frontend/vite.config.ts under the test key, which means it shares Vite’s module resolution and transform pipeline:
test: {
  globals: true,
  environment: 'jsdom',
  setupFiles: './src/test/setup.ts',
  css: true,
}
  • environment: 'jsdom' gives your tests a browser-like DOM.
  • globals: true means you don’t need to import describe, it, expect — they’re available globally, just like Jest.
  • setupFiles runs src/test/setup.ts before each test file — this is where global test setup lives (custom matchers, mock resets, etc.).

Running frontend tests

make test module=frontend

Where tests live

Tests always live in a __tests__/ directory right next to the code they test. The pattern is the same for backend and frontend — co-locate the test folder with the source file. Backend:
src/
└── tools/
    └── queue/
        ├── loader.ts
        └── __tests__/
            └── loader.spec.ts
Frontend:
src/
└── components/
    ├── passwordInput.tsx
    └── __tests__/
        └── passwordInput.spec.tsx
This keeps tests easy to find — you never have to search a separate test tree.

Mocking patterns

The backend uses Jest and the frontend uses Vitest. Their mock APIs are nearly identical, but the import is different — jest.* for backend, vi.* for frontend.

Backend: mocking dynamic imports (loader tests)

The queue, logger, and metrics loaders use dynamic import() to load the right implementation at runtime. In tests, you mock these at the module level with jest.mock:
jest.mock('tooling-queue-bullmq', () => ({
  buildBullMQQueueClient: jest.fn(),
}));

jest.mock('tooling-queue-sqs', () => ({
  buildSQSQueueClient: jest.fn(),
}));
This intercepts the dynamic import before any code runs and replaces it with a mock — so you can test routing logic without needing real Redis or SQS.

Backend: mocking Stripe operations

The tools.paymentProcessor object is a plain object of functions, which makes it trivially mockable with jest.spyOn:
jest.spyOn(tools.paymentProcessor, 'createSubscription')
  .mockResolvedValue({ success: true, result: mockSubscription });
In the test environment, the payment processor is already configured as stripeMock — a no-op stub that returns { success: false } for everything. Use jest.spyOn when a test needs a specific successful response. Never mock the real Stripe SDK.

Backend: the tools object

Most backend tests that touch database or external services receive a tools object. The test environment wires this up via apps/backend/src/configuration/test.ts. You don’t construct it manually — it’s the same dependency-injection pattern as production, just with mock or test implementations substituted in.

Frontend: mocking with Vitest

Frontend tests use vi.fn() and vi.mock() — the Vitest equivalents of Jest’s mock utilities:
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MyComponent } from '../MyComponent';

// Mock a module
vi.mock('@/hooks/useAuth', () => ({
  useAuth: vi.fn().mockReturnValue({ user: { id: '1' } }),
}));

describe('MyComponent', () => {
  it('renders correctly', () => {
    render(<MyComponent />);
    expect(screen.getByText('Hello')).toBeInTheDocument();
  });

  it('calls handler on click', () => {
    const handleClick = vi.fn();
    render(<MyComponent onClick={handleClick} />);
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalled();
  });
});
@testing-library/react is already set up — use render, screen, and fireEvent for component tests. The jsdom environment means document and DOM queries work exactly as you’d expect in a browser.

Tool package tests

Tool packages (apps/tools/*/*) also have Jest configs and their own __tests__/ directories. Run them with:
# From the repo root, runs all tool package tests
make test-modules
These run as part of the integration.yml CI pipeline in the tests job.

What’s next?

  • Continuous Integration — how tests run in CI, coverage diffing, and the codegen-verification job.
  • TypeScript — how ts-jest picks up the TypeScript config.
  • Linter — keeping code clean before it ever gets to tests.