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/findUniqueimplementation intests/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_testand 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-reactis mocked withvi.mockso tests controlisAuthenticated,userandgetAccessTokenSilentlywithout a real provider. - User interaction. Flows use
userEvent.setup()for realistic typing/clicking andwaitFor/findByfor asynchronous UI updates, as inreport-problem.test.jsx. - Environment switching.
dev-proxy.test.jsdeclares// @vitest-environment nodeand boots a real Vite dev server plus a stub upstream HTTP server to prove the proxy forwards/api/v1requests with the authorization header and strips theoriginheader.
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 duringtest:coverage. - CI uploads
coverage/lcov.infousing the Codecov action with a per-repository flag (api/frontend). - Each repository's
codecov.ymldefines 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 must come from a passing CI run (
- 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
- Lint & format —
npm ci, thennpm run lintandnpm run format:check. No style violations allowed past this stage. - Unit & UI tests —
npm run test:coverageon GitHub (with Codecov upload);npm teston Gitea. The Gitea variant runs the suite without coverage so the pipeline stays fast and self-contained. - 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]
- Lint & test —
npm ci, Prisma client generation (npm run db:generate),npm run format:check,npm run lint, thennpm run test:coveragewithNODE_ENV=testand test-only Auth0/database environment values. The PostgreSQL suites skip automatically becauseB5_TEST_DATABASE_URLis unset in CI. - Deploy — on pushes to
mainonly, 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¶
- Place the file correctly. Backend:
toodle-api/tests/unit/**ortests/integration/**, named*.test.js. Frontend:Toodle/tests/, named*.test.jsor*.test.jsx. - Follow the existing style.
describe/itwithexpect, arrange-act-assert ordering, andbeforeEach/afterEachfor state resets. - 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. - Cover the policy cases. Success, failure and role-based cases for feature changes; a reproducer for bug fixes (see Testing Policy).
- Run locally before pushing.
npm test(ornpm run test:coverage) in the repository you changed. - 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+ dedicatedb5_testdatabase).
Reproducibility notes¶
- On Windows PowerShell, run
npm.cmdinstead ofnpmwhere 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 installwith 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.