When I run
go test ./foo, Go toolchain performs several tricks under the hood.
First — skipping the intermediate preparation steps, like parsing the command line flags and checking for cached results — the toolchain generates a package
main, which will run all
TestXxx functions for a package under the test. Then it compiles a testing-binary, that includes the generated code, and the code of the package and its _test.go files.
The steps above are somewhere equivalent to running the following command:
> go test -c -o foo.test ./foo # The resulting "foo.test" is indeed an executable > file foo.test foo.test: Mach-O 64-bit executable x86_64
This binary includes a generated function “main”, that, eventually, calls the packages
TestMain(m *testing.M) (if one exists), and executes all the
TestXxx(t *testing.T) functions, which the toolchain found during the code-generation.
For the details of how the generated “main” looks like, refer to the source code of the internal package “load”.
Next, the toolchain executes the built binary, setting the current working directory to the path of the original package
foo, which is equivalent to running the following:
> cd ./foo > ./../foo.test
The important (and sometimes not obvious) part is that Go toolchain builds a dedicated binary for every package, under the testing scope. That is, in the example above, the testing binary contains only the code of the package “foo”, its dependencies, and the code in its _test.go files. If we ran the tests for several packages —
go test ./pkg/... — the toolchain will generate, built and execute individual binaries for every package below
pkg/. This makes the testing of each package fully-isolated.
There are lots of other things happening underneath, including code coverage, benchmarking, etc. Have a look through the documentation under
go help test and
go help testflag, for some more details.