Skip to content

The package clause Line That Still Passes Build, Vet, and Test

Adityo Guni Waluyo

A wrong package clause on a moved Go test file still passes go build, go vet, and go test. The import-binding rule behind it, and the procedural guard.

TL;DR

Moving a Go test file to a new directory silently broke its package clause: it still said package servicetype instead of servicetype_test. The build stayed green because unaliased imports share the import's name, so every servicetype.X call hit the import legally. Lesson: after relocating tests, manually check the package clause on line one, since tooling won't catch it.

One line, package servicetype, where it should have been servicetype_test.

While reviewing the test-relocation task into the central tree on the KotaPortal (alias) repo, my eyes stopped on the first line of a freshly moved integration file: the package clause still read package servicetype, not package servicetype_test. The task moved tests into api/tests/integration/servicetype/, and that file had slipped past me in an earlier iteration. My assumption back then: the toolchain would catch this first. A wrong clause means the wrong package, and go build would surely scream before I even looked.

Everything stayed green instead. go build exited 0, go vet said nothing, go test passed. The wrong line got past three gates at once, and it only stopped because of human review.

Why it still compiled

The key is how Go binds names. The language specification says: "If the PackageName is omitted, it defaults to the identifier specified in the package clause of the imported package" [1]. That means the import import "…/servicetype" without an alias declares an identifier named servicetype in the file. If the file's clause still says package servicetype, every servicetype.X() reference is treated as a call on that import. Syntactically, nothing is broken.

And why is an import allowed to share a name with the file's own package at all? Because the rule is not about names, it is about shape: "It is illegal for a package to import itself, directly or indirectly" [1]. In the old location, the test file lived inside the servicetype package's directory, and "Each package within a module is a collection of source files in the same directory that are compiled together" [3]. Package identity equals directory. Importing your own package from inside itself is an import cycle, and the compiler rejects it. That file could never silently carry a wrong clause at its original home; it would not even compile in that shape.

After the move to tests/integration/servicetype/, the file's package path changed [3], the cycle ban disappeared, and the same-named import became legal. A mechanical relocation changed which language rules applied to identical code. I proved the pattern in a separate minimal module: a test file with the clause package svc plus a same-named import, in the new directory, passed go build, go vet, and go test; the same file in its original directory failed with "import cycle not allowed in test".

Gates that check different things

The black-box testing convention states that a file in a separate _test package "must be imported explicitly and only its exported identifiers may be used" [2]. Note the key word: the tested package must be imported explicitly, and the compiler does enforce the consequence. With the servicetype_test clause, every internal symbol becomes unreachable; one reference to an internal function turns red immediately. That is indirect enforcement: the compiler validates identifier reachability, not the clause text. Because every symbol used in this commit was already exported, the wrong clause never touched that enforcement, so nothing turned red.

The lesson I took: green lights measure the code as written, not the package relationship you intended. go build/go vet/go test answer "does this compile and run", not "does this file test the package from outside as intended". Architectural intent has no automatic gate here; the guard is procedural. In the fix commit, the only change was flipping the clause to servicetype_test; not one other line.

A small ritual after moving tests

Since then, my checklist for Go test relocations gained one item: after everything is green, open the diff and look at the first line of every moved file. That one line is what you cannot delegate to the toolchain. Another case in the same commit arc confirms the pattern: a handler test file that only touched auth primitives was moved and its clause changed with it, admin_test to auth_test, with the unused imports dropped. Moving means changing identity, and that identity is written on the first line.

Sources

  1. The Go Programming Language Specification
  2. Package testing
  3. Go Modules Reference

Related articles