Published Oct 7, 2021
I feel very strongly that program testing should be done through public interfaces, and there needs to be a really good reason to do otherwise. One of the things I like about Go is that its tooling makes it easy for me to say that all testing for any package must be done with only the public interface.
How can that be?
Aren’t there complicated things that should be tested but not made public?
But there are two conventions that make this possible:
_test packages and
I’m writing this up because this pattern is something I used extensively when I started writing Go in late 2017, but I couldn’t find the place where I learned it.
Even better, as I was telling a teammate that the Go documentation was gaslighting me, I learned that
_test packages have been under-documented for literally years.
There’s a single-sentence paragraph that describes them in the package docs for
cmd/go (since this is a
go feature, and not a language feature).
Test files that declare a package with the suffix “_test” will be compiled as a separate package, and then linked and run with the main test binary.
_test package recipe
When testing a package
app/foo (say, with a file
bar.go), create your test file with the
_test.go suffix as usual.
In this case, that would be
However, instead of putting
package foo at the top like you normally would, use
package foo_test instead.
foo_test package is now totally independent from
foo (other than living in the same directory).
If you want to use anything from
foo in your tests (which you do, right?), you’ll need to import it just like its other consumers do.
This means that you’re forced to only interact with exported names. If something feels awkward in these tests, it will almost certainly feel awkward to consumers.
As far as I know, this is the only special case where a directory can contain two different
Now in an
So now that we know how to write public interface tests, let’s go full “strict mode” and ban all private tests. Essentially, if it can’t be observed by consumers through the public interface, it must not be tested.
I wouldn’t expect anyone to actually take this stance. But I can tell you that I wrote Go in a team for 2 years and just realized that I don’t even have a naming convention for private test files that coexist with public ones. So you might be able to get pretty far!
Anything that’s complicated enough to test but not useful (or stable!) enough to be exported can move into an
app/foo.complicated function could become
app/foo/internal/utils.Complicated (used in
app/foo/internal/utils package can only be imported by
app/foo and its
utils_test package, so it’s not really exported.
And you’re now back to writing public tests (for an internal package) instead of private tests!
Discovering the package boundary
This works really well for “evolved abstractions”.
app/foo seems to have developed a clear abstraction around
One way to tell that this has happened is if there’s a subset of names that include “Item”:
type ItemSet, etc.
item package seems useful, and it might even be helpful elsewhere.
(Maybe it’s even good enough to open-source!)
But it’s probably missing some things (documentation, good error messages, etc.) before it’s ready to be used generally throughout the codebase.
Item and friends to
While things settle from that refactor, it’s easy to move things into or out of the
And with tests in the
item_test package, you’ll uncover awkward constructions before you commit support for them.
In writing this, I realized I’m essentially rephrasing the same Dave Cheney post from before, but I hope the extra detail is useful along with his philosophy.
A full example
Here’s a code dump of what this looks like for a totally useful package in a totally real app I just made.
$ tree . ├── go.mod └── useful ├── internal │ └── splines │ ├── splines.go │ └── splines_test.go └── useful.go 3 directories, 4 files
module app go 1.17