Test a Go CLI Program Using TestMain and exec.Command
Test a Go command line program by building it once in TestMain and running it with exec.Command in each test, checking output and exit codes with ExitError.
A Go command line tool should be tested the way a user runs it, with real arguments and a real exit status.
export HOME=/tmp
mkdir cli && cd cli
go mod init cli 2> /dev/null
cat > main.go <<'EOF'
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: cli NAME")
os.Exit(2)
}
fmt.Println("hello", os.Args[1])
}
EOF
cat > main_test.go <<'EOF'
package main
import (
"errors"
"os"
"os/exec"
"path/filepath"
"testing"
)
var bin string
func TestMain(m *testing.M) {
dir, _ := os.MkdirTemp("", "cli")
bin = filepath.Join(dir, "cli")
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
panic(string(out))
}
code := m.Run()
os.RemoveAll(dir)
os.Exit(code)
}
func TestGreets(t *testing.T) {
out, err := exec.Command(bin, "ann").Output()
if err != nil || string(out) != "hello ann\n" {
t.Fatalf("got %q, %v", out, err)
}
}
func TestUsageExitCode(t *testing.T) {
err := exec.Command(bin).Run()
var ee *exec.ExitError
if !errors.As(err, &ee) || ee.ExitCode() != 2 {
t.Fatalf("want exit 2, got %v", err)
}
}
EOF
go test -count=1 -v .=== RUN TestGreets
--- PASS: TestGreets (0.00s)
=== RUN TestUsageExitCode
--- PASS: TestUsageExitCode (0.00s)
PASS
ok cli 0.101sTestMain runs before the tests of a package, so it is the place to build the binary once with go build into a temporary directory and to remove it afterward. Each test then calls exec.Command(bin, args...); Output captures what the tool printed, and errors.As with *exec.ExitError gives access to ExitCode() for the failure case. Both tests passed, one for the greeting and one for exit status 2 without arguments. See TestMain in the testing package.
Building takes a moment, which is why it happens once and not per test. Since Go 1.15, returning from TestMain exits with the status of m.Run, but the explicit os.Exit shown here is needed to run cleanup code after it. Tests that only call functions are faster, so keep the exec tests for the behavior of the whole program. This was run with Go 1.19.