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!