A Relative Test Path That Went Astray
A mechanical test-tree reorganization broke migration tests: relative paths bind to directory depth, not module root. One level deeper and the ladder ran out.
TL;DR
A directory reorg broke migration tests because go test runs from the test file's location, not the module root. The hardcoded relative path depth ladder went stale, resolving one level short of the migrations folder. Fix: re-derive paths from the new location and list attempted depths in failure messages.
After running go test following a project directory reorganization, all migration tests failed suddenly. The fatal message was monotonous: failed to create migrate instance (tried ../../ and ../../../). Nothing changed in the migration logic itself, only the test file moved to a new directory tree.
The Misleading Assumption and Root Cause
The move commit explicitly stated that the relative path to the migrations folder stays correct at the new location with the same depth. This claim was technically true for migrations_test.go, which was moved at equal depth. However, the claim turned out to be misleading when applied to the shared integration test modules. The failure did not surface at the time of the move but one commit later, when the deeper test modules started running.
The migration driver used in this project, golang-migrate/migrate [6], documents support for two file source forms: file:///absolute/path and file://relative/path [5]. The project used the relative form. The fundamental problem lies in how Go handles the working directory during testing.
In local directory mode, go test compiles the package sources and tests found in the current directory and then runs the resulting test binary [7]. The direct consequence is that the test process working directory is bound to where the test file lives, not to the module root.
When the tests/integration directory tree was moved one level deeper than the old internal location, the depth ladder in the SetupTestDB helper in the testutil package became invalid. This helper was previously hard-coded to try 2 then 3 levels up, a pattern designed for the old directory topology. From the new location at depth four, the upward ladder ran out of rungs before reaching the target.
The case is concrete: the file://../../migrations path hardcoded in migrations_test.go now resolves to api/tests, one level short of the migrations folder. The relative path goes astray because it is bound to directory depths that are already stale. This is why a move that looked safe broke the migration tests one commit later.
Adjusting the Depth Ladder
What makes this incident instructive as much as frustrating: every other unit test passed. Only tests touching the migrations folder fell, because they alone depend on a relative path. The fix required re-deriving path resolution from the new location, not copying the old configuration. The depth ladder in SetupTestDB was re-centered to try 3 then 4 levels up. The change was accompanied by a code comment that explicitly names the new depths to prevent future regressions.
In parallel, the standalone migrations_test.go was updated to hardcode a 3-levels-up path (file://../../../migrations), aligned with the new directory tree. Its fatal message was also updated to list the new depth pair so that the next failure reads itself. After these adjustments were applied, the down/up migration cycle was re-run against the test database. The terminal output confirmed the schema was torn down and rebuilt without the old directory not found error, confirming the path resolution is now accurate and stable.
Two lessons fall out of this that apply immediately. First, treat every relative path in tests as a depth-bound constant: moving a test file changes that constant whether you like it or not. Second, a depth ladder like SetupTestDB's is pragmatic for a small repo but hides topology assumptions in rarely-read code. A comment naming the depths tried, plus a fatal message listing them, is a cheap price for failures that diagnose themselves.
Relative resource paths in testing are tightly bound to the depth of the directory where the test file lives. When the test directory tree is reorganized, re-derive every relative path from the new location. As a defensive practice, add an upfront assertion that verifies the resolved path exists. If the path is missing, emit a fatal message listing all depths tried, so the failure explains itself without requiring a second guess.