If you look at the
tests for
the Go standard library’s os/exec
package, you’ll find a neat trick
for how it tests execution:
func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) {
testenv.MustHaveExec(t)
cs := []string{"-test.run=TestHelperProcess", "--"}
cs = append(cs, s...)
if ctx != nil {
cmd = exec.CommandContext(ctx, os.Args[0], cs...)
} else {
cmd = exec.Command(os.Args[0], cs...)
}
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
// TestHelperProcess isn't a real test.
//
// Some details elided for this blog post.
func TestHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
cmd, args := args[0], args[1:]
switch cmd {
case "echo":
iargs := []interface{}{}
for _, s := range args {
iargs = append(iargs, s)
}
fmt.Println(iargs...)
//// etc...
}
}
When you run go test
, under the covers the toolchain compiles your
test code into a temporary binary and runs it. (As an aside, passing
-x
to the go
tool is a great way to learn what the toolchain is
actually doing.)
This helper function in exec_test.go
sets a GO_WANT_HELPER_PROCESS
environment variable and calls itself with a parameter directing it
to run a specific test, named TestHelperProcess
.
Nate Finch wrote an excellent blog post in 2015 on this pattern in greater detail, and Mitchell Hashimoto’s 2017 GopherCon talk also makes mention of this trick.
I think this can be improved upon somewhat with the
TestMain
mechanism
that was added in Go 1.4, however.
Here it is in action:
package myexec
import (
"fmt"
"os"
"os/exec"
"strings"
"testing"
)
func TestMain(m *testing.M) {
switch os.Getenv("GO_TEST_MODE") {
case "":
// Normal test mode
os.Exit(m.Run())
case "echo":
iargs := []interface{}{}
for _, s := range os.Args[1:] {
iargs = append(iargs, s)
}
fmt.Println(iargs...)
}
}
func TestEcho(t *testing.T) {
cmd := exec.Command(os.Args[0], "hello", "world")
cmd.Env = []string{"GO_TEST_MODE=echo"}
output, err := cmd.Output()
if err != nil {
t.Errorf("echo: %v", err)
}
if g, e := string(output), "hello world\n"; g != e {
t.Errorf("echo: want %q, got %q", e, g)
}
}
We still set an environment variable and self-execute, but by moving
the dispatching to TestMain
we avoid the somewhat-hacky special test
which only ran when a certain environment variable is set, and which
needed to do extra command-line argument handling.
Update: Chris Hines wrote about
this and other useful things
you can do with TestMain
in a post from 2015 that I did not know
about!