Friends in Go
A best practice when writing tests is to put them in a separate _test
package.
This forces you to use your API as a consumer and ensures that your tests aren’t coupled to your implementation detailsTests that you have to change every time you change the internals of a package are tech debt..
It is a great way to avoid test-induced design damage like overmocking.
That said, whitebox testing has its place. Sometimes when writing these kinds of tests, it’s nice to be able to peer inside a package and inspect things you wouldn’t want to expose to consumers.
A neat trick is using export_test.go
to redeclare the identifiers you want to export, kind of like the friend
keyword in C++.
This works as test files are excluded from regular package builds but included when the go test
command is run.
package foo
var Foo = foo
var FooType fooType
This pattern can be seen in the math
and net/http
packages from the Go standard library.
By convention, the file that exposes internals to tests is named export_test.go
.
// math/export_test.go
package math
// Export internal functions for testing.
var ExpGo = exp
var Exp2Go = exp2
var HypotGo = hypot
var SqrtGo = sqrt
var TrigReduce = trigReduce
const ReduceThreshold = reduceThreshold
// net/http/export_test.go
package http
var (
DefaultUserAgent = defaultUserAgent
NewLoggingConn = newLoggingConn
ExportAppendTime = appendTime
ExportRefererForURL = refererForURL
ExportServerNewConn = (*Server).newConn
ExportCloseWriteAndWait = (*conn).closeWriteAndWait
ExportErrRequestCanceled = errRequestCanceled
ExportErrRequestCanceledConn = errRequestCanceledConn
ExportErrServerClosedIdle = errServerClosedIdle
ExportServeFile = serveFile
ExportScanETag = scanETag
ExportHttp2ConfigureServer = http2ConfigureServer
Export_shouldCopyHeaderOnRedirect = shouldCopyHeaderOnRedirect
Export_writeStatusLine = writeStatusLine
Export_is408Message = is408Message
)
...
func init() {
// We only want to pay for this cost during testing.
// When not under test, these values are always nil
// and never assigned to.
testHookMu = new(sync.Mutex)
testHookClientDoResult = func(res *Response, err error) {
if err != nil {
if _, ok := err.(*url.Error); !ok {
panic(fmt.Sprintf("unexpected Client.Do error of type %T; want *url.Error", err))
}
} else {
if res == nil {
panic("Client.Do returned nil, nil")
}
if res.Body == nil {
panic("Client.Do returned nil res.Body and no error")
}
}
}
}
...
func init() {
// Set the default rstAvoidanceDelay to the minimum possible value to shake
// out tests that unexpectedly depend on it. Such tests should use
// runTimeSensitiveTest and SetRSTAvoidanceDelay to explicitly raise the delay
// if needed.
rstAvoidanceDelay = 1 * time.Nanosecond
}
As an aside, it turns out Go lets you can have as many init
functions as you want!
A package is initialised by assigning initial values to all its package-level variables followed by calling all
init
functions in the order they appear in the source, possibly in multiple files, as presented to the compiler.
Circular dependencies
You may be familiar with the import .
syntax, which pulls all of a package’s exported identifiers into the current namespace.
This is handy when you’re writing a test that cannot be made part of the package due to circular dependencies.
package foo_test
import (
"bar/testutil" // also imports "foo"
. "foo"
)
This is used in net/http/serve_test.go
to be able to use various subpackages.
package http_test
import (
...
. "net/http"
"net/http/httptest"
"net/http/httptrace"
"net/http/httputil"
"net/http/internal"
"net/http/internal/testcert"
...
)
...
According to the Go Code Review Comments, this is the only time you should use import .
!
You should avoid it otherwise:
It makes programs much harder to read because it is unclear whether a name like Quux is a top-level identifier in the current package or in an imported package.