conduit-full — testing strategy

Real in-memory SQLite over hand-stubbed mocks. Every fix proven by reverting it first.

testUtils/testDb.ts spins up a genuine SQLite Sequelize instance running the real models for every controller/route test — controllers touch too many Sequelize methods for per-method mocking to stay maintainable, and a mock can't catch a wrong foreign key the way a real query against a real schema does. See Data Model for the associations these tests actually exercise.

Four Layers, Bottom to Top

Migrations Every migration test cross-checks its column shape against the real model — this is the layer that would have caught the missing userId/articleId FK columns before they ever shipped.
Models A capstone test wires every model's real associations end to end — not just that each association compiles, but that the whole graph resolves together once all four models exist.
Controllers / routes Real in-memory SQLite via testUtils/testDb.ts, not hand-stubbed mocks — 214 tests total across every controller and route.
End to end backend/index.test.ts drives a full register → login → create-article → list-articles journey through the real, fully-wired Express app.

Two Habits Applied Throughout

Revert and confirm

Every meaningful fix, proven twice

The pattern used for essentially every real bug in this repo: revert the fix, watch the test fail for the right reason, restore the fix, watch it pass again. Applied to the JWT expiration fix, the double-next() bug, the wrong FK association, and — on the frontend — errorHandler.test.ts's mock rewrite.

Test-infra bugs get flagged as such

Not every failure is an app bug

The TypeScript retrofit surfaced a case where a static top-level import and a dynamic import() after vi.resetModules() ended up in different module "epochs" with different class identities, so instanceof silently failed — a real bug, but in the tests, not the app. Logged as exactly that in todo.md, not folded into the app-bug count.

i

The same discipline carries into the frontend: errorHandler.test.ts's original mock built error.response from the Fetch API's Response class, which has no .data field — its assertion was passing because of an unrelated crash, not the code path it claimed to test. Caught by actually reading what the mock produced, not by trusting a green checkmark.