2026-07-13
Refactor Milestone: Making TDD Real
Before this session, "TDD project" was a claim nobody had checked. I confirmed it the way I should have from the start: no test project under beer-app/backend, no test script in beer-app/frontend/package.json, no .github/workflows folder at all. Zero tests, on either side of the stack, for a codebase that already has a BeersController, an AuthController, and four live React pages.
Backfilling instead of starting clean
The goal wasn't exhaustive coverage for its own sake — it was baseline coverage for what already exists: happy paths and the obvious edge cases (missing beer, wrong password, duplicate email), not every branch. An xUnit project, beer-app/BeerApi.Tests, covers the backend; Vitest and React Testing Library cover the frontend, colocated as *.test.jsx next to each page.
The one piece that needed real care: [Authorize(Roles = "Admin")] on BeersController is enforced by ASP.NET's middleware pipeline, not the action method itself. A unit test that calls the controller method directly would pass even if authorization were completely broken, because it never goes through the pipeline that checks it. That behavior only gets tested for real by making actual HTTP calls with WebApplicationFactory<Program> — anonymous, unauthenticated, wrong-role, and Admin, checked against real 200/401/403/201 responses.
Two things the suite found before I did
Writing the tests surfaced two real issues, neither obvious until something actually exercised the code:
SeedData.csseeds a shared static array ofBeerobjects. Fine for a real process that starts once, but every integration test class boots its ownWebApplicationFactory, and xUnit runs test classes in parallel by default — three app instances racing to mutate the same shared entity instances at once. Fixed by serializing those test classes into one xUnit collection, not by touching the seed data itself, since the concurrency only exists because of the test harness.Program.cscalleddb.Database.Migrate()unconditionally, which throws against EF Core's InMemory provider (used by the tests) since it only supports relational providers. Fixed with anIsRelational()check so Postgres still migrates normally and the test host falls back toEnsureCreated().
The mess that had nothing to do with tests
While staging the commit, git status turned up something unrelated: this repo never had a .gitignore. Compiled .dlls, node_modules, even Vite's dist output had been committed and drifting for two years. I fixed it separately — its own branch and PR, rather than folding it into the test work — with a .gitignore and git rm -r --cached on about 2,600 tracked files. I also tested what rewriting git history to actually shrink the repo would look like, in a throwaway clone: 46MB down to 24MB. Then I didn't push it. That would mean force-pushing over master, and this project's own SESSION_LOG.md treats commit history as a real process record, not disposable. A smaller .git folder wasn't worth erasing that trail.
The PM angle
Both pieces — the tests and the repo cleanup — merged to master the same session, as two separate PRs, CI green on the one that had CI to check. Neither was asked for by a roadmap; both came from actually looking at the repo instead of assuming it matched what the docs said. It's the same lesson as the foundation-hardening session, just showing up somewhere else: a claim like "this is a TDD project" is only true once something checks it, and until then it's indistinguishable from a claim that happens to be false.
Comments
Loading comments...