DEV Community

Ian Ochieng
Ian Ochieng

Posted on

# Building SemaKazi Part 3: Adding Tests (and Why I Had to Refactor First)

SemaKazi is a verified reputation platform for Kenya's informal-sector workers — electricians, carpenters, mechanics, tailors — solving the trust gap between a fundi's real skill and a client's ability to verify it before hiring. Backend shipped in Phase 1, frontend (auth, search, profiles, dashboard) shipped in Phase 2. Today was Phase 4: tests.

The problem with testing an Express app that just... runs

My server.js did two jobs at once: configure the Express app and call app.listen() to start it on a fixed port. That's fine for running the app, but it makes it nearly impossible to test cleanly — you can't import "the app" without also starting a real server bound to a real port, which collides with whatever's already running in dev.

The fix is a pattern I'd read about but never actually needed until today: split it in two.

  • app.js — builds and returns a configured Express app. No listen() call.
  • server.js — imports that app, calls .listen(), and that's it.

Now tests can import app.js, spin up an instance on port: 0 (which tells the OS "just give me any free port"), run requests against it, and tear it down — no collisions, no fixed ports, no interference with a dev server that might already be running.

What got tested

Seven tests, using Node's built-in test runner and native fetch — no new dependencies:

  • Health check responds correctly
  • Registration creates a user and returns a token
  • Duplicate email registration is rejected
  • Login succeeds with correct credentials, fails with wrong ones
  • Protected routes reject requests with no token
  • Search returns fundis with correct average rating and review count
  • A user cannot edit another user's profile (ownership check)

Each test run gets its own throwaway SQLite file, cleaned up after — so tests never touch the real dev database.

Why this specific set

Not exhaustive coverage — just the kinds of things that already broke once. The .env/JWT issue from Phase 1 and the profile.js/profiles.js naming mismatch were both only caught by manual curl testing at the time. A real test suite means the next regression like that gets caught automatically instead of by trial and error in a terminal.

Small refactor, real lesson

The actual code that broke in the past week wasn't complicated. What made debugging slow was structure — a server file that couldn't be tested in isolation, a file name that silently worked in one environment and failed in another. Fixing the shape of the code turned out to matter more than adding more code.

What's left

One small frontend polish item — the API base URL is currently hardcoded to localhost, which needs to be configurable before deployment — then Phase 5: getting this live.


Repo's public if you want to see the test file or the before/after of the refactor.

Top comments (0)