The Category CRUD Was Done. The Tests Did Not Exist
Shipping nested categories with zero tests worked until the QA checklist asked for proof: the boot env is a contract, and constraint edges need a real MySQL.
TL;DR
Nested categories were live with no test coverage, and even the health check was broken since it missed required env vars. I patched the env setup and added three MySQL-backed integration tests for tricky deletes that mocks would hide. They run with the integration tag and -p 1 to avoid collisions on the shared test database.
The S5.5 QA checklist had one item left when I opened category_repository_test.go for the first time. The file had been created that very day. Which means one thing made me pause for a second: the nested category feature, complete with create, nest, reorder, and delete, was already alive in the app, and not a single test was watching it. Everything had been green all along not because the code was healthy, but because nothing was being tested.
My first guess was optimistic: add one happy-path test, close the checklist, go home. Turns out the first thing to fix was an old test. TestHealthEndpoint already existed, but its server never started. The run() function refuses to boot unless the DB_* variables and JWT_SECRET are all set, and the test set none of them.
The test env is a contract, not a detail
The fix was six lines of t.Setenv pointing at the test database on port 3307, but the lesson is bigger: every env var read at boot is a contract, and any test calling run() has to fulfill it. The host and credentials point at the test database, not the one used for development.
// run() refuses to start without these - point the test at the test DB
t.Setenv("DB_HOST", "<db-host>")
t.Setenv("DB_PORT", "3307")
t.Setenv("JWT_SECRET", "test-secret")t.Setenv has its own limits: it restores the old values through Cleanup, and because the effect is process-wide it is forbidden in parallel tests [8]. Six innocent-looking lines still need to stay in their place.
Three edges that only show up against a real MySQL
The core of the checklist was never the happy path but three edges: deleting a parent that still has children, deleting a category still used by entities, and deleting an unknown id. The first two must return ErrCategoryInUse, the last one ErrCategoryNotFound. Against a mock, I could make every scenario "pass" myself, because a mock returns whatever I programmed. That is exactly the blind spot: error mapping that seems obvious can drift from actual MySQL behavior, and only a test against the real database will find the difference.
The test file itself is locked behind //go:build integration, so it only joins the run when go test gets -tags integration [7]. Without the tag the builder ignores the file, and I'm back to the green illusion. It's a trade-off I accept: integration tests shouldn't run with unit tests on every compile, but the cost is one conscious step to invoke them.
-p 1: being honest about a shared database
The suite passed, but only with one extra flag: -p 1. The -p flag controls how many test binaries run in parallel, and its default is GOMAXPROCS [7]. Our test database is one database shared across packages, so when two packages run at once, one can rip out a category another test is using. The race isn't in the application code, it's in fixtures stomping over the same tables.
Go actually gives you fine-grained control over which tests run and how parallel they are from the command line [6], so this is a choice, not a limitation. I picked -p 1 as a temporary honesty: slower execution, deterministic results. The commit note is honest too, this is a pre-existing issue to chase later, and the real fix is per-test data isolation.
What changed in this session isn't just four new tests. Two things that used to be "I'm sure" now have tools attached: the boot env is a contract written down in tests, and constraint edges are claims a real database can break. That's how the S5.5 checklist finally closed.
Sources