Skip to content

Automated Testing Procedure

This page describes the automated test suites in the Toodle (frontend) and toodle-api (backend) repositories, how to run them, what each layer covers, and how the CI pipelines execute them automatically.

Tooling overview

Concern Backend (toodle-api) Frontend (Toodle)
Test runner Vitest 5 Vitest 3
Environment Node jsdom (one file opts into Node)
HTTP testing Supertest against the real Express app Not applicable (UI level)
UI testing Not applicable Testing Library (render, screen, waitFor) and user-event
Database Mocked Prisma client by default; real PostgreSQL opt-in Not applicable
Coverage @vitest/coverage-v8, HTML/LCOV/Cobertura @vitest/coverage-v8, text/LCOV
External coverage Codecov Codecov
Lint / format ESLint + Prettier ESLint + Prettier

Backend (toodle-api)

The backend suite lives in toodle-api/tests/ and is split into unit, integration and helper layers, with a shared setup file.

Layout

tests/
├── setup.js                     # global test setup (Prisma mock factory)
├── unit/
│   ├── app.test.js              # app assembly smoke test
│   ├── ownership.test.js        # resource ownership rules
│   ├── allocation-validation.test.js
│   ├── auth-account-deletion.test.js
│   ├── swap.test.js
│   ├── controllers/             # 7 controller test files
│   ├── services/                # 9 service test files
│   ├── middleware/              # error, RBAC, validation middleware
│   └── validators/              # Zod schema validation
├── integration/
│   ├── routes/                  # auth, course-applications, swap routes
│   ├── auth-deletion.postgres.test.js   # opt-in PostgreSQL
│   └── swap.postgres.test.js            # opt-in PostgreSQL
└── helpers/
    ├── controller-mock.js       # Express controller test doubles
    ├── swap-database.js         # in-memory fixtures for swap scenarios
    └── test-jwt.js              # mocked Auth0 JWT middleware + token factory

Layer 1 — Unit tests

Unit tests verify individual controllers, services, middleware and validators in isolation.

The database boundary is mocked through tests/setup.js, which exposes a global createMockPrisma() factory. Every Prisma model (user, course, allocation, sessionSwap, etc.) is built from the standard method names (findUnique, findMany, create, update, deleteMany, ...) as vi.fn() spies, plus a $transaction that runs the callback against the mock. Test files import the factory inside vi.hoisted() blocks so the mock exists before the module graph loads:

const { mockPrisma } = vi.hoisted(() => ({ mockPrisma: createMockPrisma() }));
vi.mock('../../../src/config/database.js', () => ({ default: mockPrisma }));

Unit tests assert business rules directly against service/controller outputs: allocation constraint validation, excusal and overflow logic, timesheet aggregation, RBAC middleware decisions and Zod validation errors.

Layer 2 — Route integration tests

Route tests in tests/integration/routes/ boot the real Express app (src/app.js) and drive it with Supertest, so request validation, middleware ordering, routing and response shapes are exercised end to end.

Two external boundaries are substituted:

  • Auth0 is mocked via tests/helpers/test-jwt.js (mockAuth / testToken), so tests can act as any role without live tokens;
  • Prisma is replaced with in-memory behaviour mocks. Where a test needs stateful persistence across repeated HTTP calls (for example, the same row being updated on a second call), the mock models implement a small in-memory array — see the upsert/findUnique implementation in tests/integration/routes/auth.test.js.

Shared scenario fixtures (users, courses, availability, overlapping sessions) live in tests/helpers/swap-database.js.

Layer 3 — Opt-in PostgreSQL suites

Two suites run against a real PostgreSQL database: auth-deletion.postgres.test.js and swap.postgres.test.js. They verify real transaction, foreign-key and persistence behaviour that the in-memory mocks cannot prove.

These suites are opt-in by design:

  • they are declared with describe.skipIf(!process.env.B5_TEST_DATABASE_URL) and therefore skip automatically in normal runs and in CI;
  • they require a disposable local database named b5_test and refuse to run against any other host/path;
  • they connect through process.env.B5_TEST_DATABASE_URL, falling back to a local-only URL that is inert without a server;
  • each test cleans up only its fixed fixture identities.

Enable them locally with a dedicated database, never the school's shared records:

# in toodle-api, with a local Postgres running
$env:B5_TEST_DATABASE_URL = "postgresql://test:test@127.0.0.1:5432/b5_test"
npm.cmd test

Running the backend suite

From the toodle-api directory:

Command Purpose
npm test Run the full suite once (vitest run)
npm run test:watch Run in watch mode during development
npm run test:coverage Run once with coverage and thresholds

Coverage is configured in toodle-api/vitest.config.mjs: it instruments src/**/*.js (excluding the server entrypoint), reports text/HTML/LCOV/Cobertura, and enforces thresholds of 70% lines, 70% functions, 60% branches and 70% statements. A run that falls below a threshold fails.

Frontend (Toodle)

The frontend suite lives in Toodle/tests/ and covers components, UI behaviour, helpers and dev tooling.

File Covers
ui.test.jsx Smoke test: the app renders the landing page under MemoryRouter with Auth0 mocked
components/ui.primitives.test.jsx Shared UI primitives
course-applications.test.jsx Student application/mark UI and organiser approval flows
session-swap.test.jsx Swap request and approval UI behaviour
report-problem.test.jsx The in-app Report Problem modal submission payload and confirmation
helpers.test.js Shared helper functions
dev-proxy.test.js The Vite dev proxy against a real local HTTP server

Test configuration lives in the test block of Toodle/vite.config.js: jsdom environment, globals enabled, tests/setup.js as setup file, CSS processing on, and coverage over src/**/*.{js,jsx} excluding the entrypoint.

Key techniques

  • Rendering with routing context. Components that depend on routing are rendered inside MemoryRouter.
  • Auth0 mocking. @auth0/auth0-react is mocked with vi.mock so tests control isAuthenticated, user and getAccessTokenSilently without a real provider.
  • User interaction. Flows use userEvent.setup() for realistic typing/clicking and waitFor/findBy for asynchronous UI updates, as in report-problem.test.jsx.
  • Environment switching. dev-proxy.test.js declares // @vitest-environment node and boots a real Vite dev server plus a stub upstream HTTP server to prove the proxy forwards /api/v1 requests with the authorization header and strips the origin header.

Running the frontend suite

From the Toodle directory:

Command Purpose
npm test Run the full suite once (vitest run)
npm run test:watch Run in watch mode during development
npm run test:coverage Run once with coverage

Documentation repository

The documentation site has no unit tests; its automated check is a strict build that fails on broken links, missing pages or invalid navigation:

python -m mkdocs build --strict

See Getting Started → Local Development for running the site locally.

Coverage and Codecov

Both application repositories publish coverage to Codecov.

  • Vitest writes an LCOV report to each repository's coverage/ directory during test:coverage.
  • CI uploads coverage/lcov.info using the Codecov action with a per-repository flag (api / frontend).
  • Each repository's codecov.yml defines the shared policy:
    • coverage must come from a passing CI run (require_ci_to_pass: true);
    • project target 70% with a 2% tolerance threshold;
    • patch target 60% — new or changed lines must be mostly covered;
    • flags carry forward between partial runs.
  • Coverage upload is advisory in CI (fail_ci_if_error: false); the hard gate is the local Vitest threshold, which does fail the test run.

The frontend workflow additionally sets CODECOV_GIT_SERVICE: github because the primary remote is a Gitea server, which the Codecov CLI cannot auto-detect.

CI pipelines

Both repositories run CI on every push and pull request to main and develop. Each repository defines the pipeline twice — under .gitea/workflows/ for the primary Gitea server and under .github/workflows/ for the GitHub mirror — with the same stages.

Frontend (Toodle)

graph LR
    A[lint-and-format<br/>ESLint + Prettier] --> B[test<br/>Vitest]
    B --> C[build<br/>Vite production bundle]
    A --> C
  1. Lint & formatnpm ci, then npm run lint and npm run format:check. No style violations allowed past this stage.
  2. Unit & UI testsnpm run test:coverage on GitHub (with Codecov upload); npm test on Gitea. The Gitea variant runs the suite without coverage so the pipeline stays fast and self-contained.
  3. Production build — runs only after lint and tests pass, and verifies the Vite production bundle compiles with the production environment variables.

Backend (toodle-api)

graph LR
    A[lint-and-test<br/>format + lint + Vitest coverage] --> B[deploy<br/>Render hook, main only]
  1. Lint & testnpm ci, Prisma client generation (npm run db:generate), npm run format:check, npm run lint, then npm run test:coverage with NODE_ENV=test and test-only Auth0/database environment values. The PostgreSQL suites skip automatically because B5_TEST_DATABASE_URL is unset in CI.
  2. Deploy — on pushes to main only, the workflow triggers the Render deploy hook. A broken test run therefore blocks deployment.

The API workflow uses a concurrency group so a newer push cancels an in-flight pipeline for the same ref.

Adding a new test

  1. Place the file correctly. Backend: toodle-api/tests/unit/** or tests/integration/**, named *.test.js. Frontend: Toodle/tests/, named *.test.js or *.test.jsx.
  2. Follow the existing style. describe/it with expect, arrange-act-assert ordering, and beforeEach/afterEach for state resets.
  3. Mock at the right boundary. Backend unit tests mock Prisma through createMockPrisma(); route tests mock Prisma and Auth0 and keep the real Express app. Frontend tests mock Auth0 and the API modules, and keep real components.
  4. Cover the policy cases. Success, failure and role-based cases for feature changes; a reproducer for bug fixes (see Testing Policy).
  5. Run locally before pushing. npm test (or npm run test:coverage) in the repository you changed.
  6. If the test needs a real database, do not wire it into the default run — use the opt-in pattern (describe.skipIf + B5_TEST_DATABASE_URL + dedicated b5_test database).

Reproducibility notes

  • On Windows PowerShell, run npm.cmd instead of npm where the npm shim is disabled; do not change the machine execution policy merely to run npm.
  • Commands run from the relevant repository root after npm ci/npm install with the locked dependencies.
  • The API PostgreSQL suites document their opt-in environment variables in the source files; follow those, and never point them at the school's shared database.
  • Record actual logs, counts and skips when a run is used as evidence — see Sprint 2 Verification for the expected format.