Resolving Circular Dependencies in Go's context Package with XTest and export_test.go
This article explains how Go developers can break circular import problems in the context package by using XTest‑prefixed helper functions together with a regular Test wrapper, and also demonstrates the export_test.go "backdoor" technique for accessing unexported symbols in black‑box tests.
While reviewing the source of Go's context package, the author discovered a small trick in the test files that helps to solve circular‑dependency issues and shared it here.
The package layout is simple:
$ tree context
context
├── afterfunc_test.go
├── benchmark_test.go
├── context.go
├── context_test.go
├── example_test.go
├── net_test.go
└── x_test.go
1 directory, 7 filesOnly context_test.go belongs to the context package itself, while the other test files use the external package name context_test . Consequently, context_test.go is a white‑box test and the rest are black‑box tests.
Functions in context_test.go are named with the XTestXxx pattern (e.g., XTestCancelRemoves ) instead of the usual TestXxx prefix, because the go test tool does not recognise them as test functions. They also accept a custom testingT interface rather than *testing.T , so they are treated as ordinary functions.
The testingT interface mirrors the methods of *testing.T and is introduced to avoid a direct import of the testing package, which would create a import cycle between context and testing . By defining the helper as XTestCancelRemoves and adding a thin wrapper in x_test.go , the cycle is broken:
package context_test
import (
. "context"
"testing"
)
// Each XTestFoo in context_test.go must be called from a TestFoo here to run.
func TestCancelRemoves(t *testing.T) {
XTestCancelRemoves(t) // uses unexported context types
}Because TestCancelRemoves follows the Test naming convention and takes *testing.T , the go test command executes it, which in turn calls the hidden XTestCancelRemoves function.
The article also revisits the "export_test.go" back‑door technique described in "The Go Programming Language" book. By declaring exported variables in a _test.go file that reference unexported functions (e.g., exposing isSpace from the fmt package as IsSpace ), black‑box tests can access internal implementation details without affecting the normal build.
package fmt
var IsSpace = isSpace
var Parsenum = parsenumBlack‑box tests can then import the package and use the exported back‑door:
package fmt_test
import (
"bytes"
. "fmt"
"testing"
"unicode"
)
func TestIsSpace(t *testing.T) {
for i := rune(0); i <= unicode.MaxRune; i++ {
if IsSpace(i) != unicode.IsSpace(i) {
t.Errorf("isSpace(%U) = %v, want %v", i, IsSpace(i), unicode.IsSpace(i))
}
}
}Since files ending with _test.go are only compiled during testing, the exported back‑door variables do not affect production binaries.
In summary, the article presents two practical testing tricks: using XTest functions together with a regular Test wrapper to break circular imports, and employing export_test.go as a back‑door to expose unexported symbols for black‑box testing.
It also recommends reading "The Go Programming Language" for deeper insights.
Go Programming World
Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.