Black Box Tests With the Test Package Suffix in Go
Declare tests in package name_test to test a Go package as its callers see it: only exported names, no import cycles, and a compile error for private access.

Go lets a directory hold two packages for testing: the package itself, and a second one named with a _test suffix. Files declared package slug_test are compiled separately and import slug like any caller. This gives you black box tests for free, in the same directory, with no configuration.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
package slug
import "strings"
func Make(s string) string { return strings.Join(strings.Fields(strings.ToLower(s)), "-") }
func helper() string { return "not exported" }package slug_test
import (
"testing"
slug "example.test/lab"
)
func TestMake(t *testing.T) {
if got := slug.Make("Hello Go World"); got != "hello-go-world" {
t.Fatalf("got %q", got)
}
t.Logf("slug.Make works through the public API only")
// slug.helper() would not compile here: "undefined: slug.helper"
}go test -count=1 -v . 2>&1 | grep -E '(slug|PASS:|ok)' | sed -E 's/ \([0-9.]+s\)//; s/\t[0-9.]+s$//; s#^ +[a-z_]+\.go:[0-9]+: # #'
echo
echo "# what the compiler says if the test reaches for an unexported name"
cp slug_test.go slug_test.go.bak
perl -pi -e 's#// slug.helper\(\) would not compile.*#_ = slug.helper()#' slug_test.go
go vet . 2>&1 | grep 'vet:' | sed -E 's#^vet: ./##'
mv slug_test.go.bak slug_test.go slug.Make works through the public API only
--- PASS: TestMake
ok example.test/lab
# what the compiler says if the test reaches for an unexported name
slug_test.go:14:11: undefined: slug.helperWhat it buys
A caller's view. The test imports the package and uses slug.Make. If you rename or hide something a caller needs, the test stops compiling, which an in-package test would not notice. The second block of the output shows the other side: with the test edited to call slug.helper(), the check reports undefined: slug.helper, because an unexported name is not part of what the package exports.
No import cycles. Suppose the test wants a helper from testutil, which imports slug for its own purposes. A test in package slug that imports testutil creates a cycle: slug (test) imports testutil imports slug. A test in slug_test imports both without trouble, because it is a different package.
Examples that read like user code. Example functions in the external package, as in the example tests article, show the package qualifier a reader would write.
A worked cycle
Imagine slug provides Make, and a shared testutil package has a helper AssertEqual which, to print nicely, calls slug.Make on the test name. Now testutil imports slug. A test file in package slug that imports testutil produces import cycle not allowed in test, and the fix people reach for is copying the helper. The external package avoids the problem: slug_test is a separate package that imports both slug and testutil, and the dependency graph stays acyclic. This situation is common once a project has shared test support code, and it is the main practical reason many teams default to the external form.
What the external package cannot do
It cannot see unexported identifiers, as the vet output above demonstrates. It cannot define methods on the package's types, because methods must be declared in the type's own package. And its package name is a convention, not a rule: the compiler accepts slug_test because the suffix _test is special-cased by the go tool, so you cannot invent slug_blackbox. Both packages can coexist in one directory, with in-package files named internal_test.go or export_test.go by habit.
When you still need the other kind
Testing an unexported function requires the in-package form or a bridge file, as laid out in testing unexported functions. The two styles coexist: put most tests in _test and keep a small internal_test.go for the awkward cases. A common house rule is "external by default, in-package when you must".
The Go command documents the arrangement under test packages, and the testing hub puts it in context: the Go testing guide.


