Writing Tests
This page documents the conventions used in the GCMS test suite, so that new tests integrate cleanly with the existing setup.
File naming
Test files live in new_tests/ and follow this naming pattern:
urN-unit.test.js— unit tests for user requirement NurN-integration.test.js— integration tests for user requirement N
Where N is the user requirement number (1–14). If a new
requirement is added, create both files using the next available
number.
Unit tests
Unit tests verify controller logic in isolation by mocking all model and external dependencies.
Mocking models
Use jest.unstable_mockModule() to mock the model module before
importing the controller:
import { describe, jest, test } from "@jest/globals";
jest.unstable_mockModule('../models/userModels.js', () => ({
...jest.requireActual('../models/userModels.js'),
postUserModel: jest.fn(),
getUserByMicrosoftIdModel: jest.fn(),
}));
describe('addUser', () => {
test('creates a user and returns 201', async () => {
const { addUser } = await import('../controllers/userControllers.js');
const userModels = await import('../models/userModels.js');
userModels.postUserModel.mockResolvedValue({ rows: [{ user_id: 'uuid' }] });
const req = { user: { microsoftId: 'ms-1' }, body: {} };
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
await addUser(req, res);
expect(res.status).toHaveBeenCalledWith(201);
});
});
Note that the controller is imported dynamically inside the test, after the mock has been declared. Static imports are evaluated before the mock takes effect.
What to assert
Good unit tests check:
The correct status code is returned
res.json/res.sendis called with the expected shapenext()is called when appropriate (for middleware)The model function is called with the expected arguments
Edge cases are handled (missing fields, not-found rows, etc.)
Integration tests
Integration tests use supertest to make real HTTP requests
against the Express app, which talks to the real test database.
Test setup
Import the app directly — do not start an HTTP server:
import request from "supertest";
import app from "../app.js";
describe("UR-N INTEGRATION - feature name", () => {
test("happy path", async () => {
const res = await request(app)
.post("/api/some-endpoint")
.send({ field: "value" });
expect(res.statusCode).toBeGreaterThanOrEqual(200);
expect(res.statusCode).toBeLessThan(600);
expect(res.body).toBeDefined();
});
});
The auth bypass middleware activates automatically because the test
scripts set NODE_ENV=test. req.user will be populated with a
stub user.
What to assert
Good integration tests check:
A valid response is returned for the happy path
Bad input is rejected with an appropriate status code
Authorisation rules are enforced
The database is in the expected state after the request
Tests should be independent — never assume an ordering of tests within a file, and never depend on state created by another test file.
Common patterns
Cleaning up between tests
Use beforeEach or afterEach hooks to reset relevant database
tables when isolation is required:
import { pool } from "../utils/supabase.js";
afterEach(async () => {
await pool.query("DELETE FROM tasks WHERE task_title LIKE 'TEST%'");
});
Testing socket events
Socket.io events are not exercised by the standard integration tests
because app.js does not include the socket layer (that is
attached in server.js). Socket-related logic should be tested at
the unit level instead.