Recently I’ve been building some CLI applications using Go and cobra, and have been looking for an easy, reproducible way to build and test their behavior by launching the binary and testing it out with actual command line arguments. Here is what I came up with using a Makefile
and exec.Command
.
Testing gecko
gecko
is a go
version of echo. It takes arguments from the command line and writes them to standard output:
gecko.go
package main
import (
"fmt"
"os"
"strings"
)
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
Test code
To test this application, we can write a test function which calls gecko
with os.Exec
, checks the contents of standard output, and then verifies it is what we expect.
gecko_test.go
package main
import (
"bytes"
"fmt"
"testing"
"os/exec"
)
func TestGecko(t *testing.T) {
cmd := exec.Command("./gecko", "hello")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if out.String() != "hello\n" {
t.Errorf("Expected 'hello' received '%s'", out.String())
}
}
Running the test
Before we run the test, we have to compile the binary.
$ go build -o gecko
$ tree
.
├── gecko
├── gecko.go
└── gecko_test.go
0 directories, 3 files
$ go test
PASS
ok github.com/pmalmgren/gecko 0.012s
$ rm gecko
Using a Makefile
It would be nice not to have to worry about compiling the test binary, running the test command, then deleting the compiled test binary. A Makefile
can help us here!
Makefile
TARGET=gecko
all: clean build test
build:
go build -o $(TARGET)
test:
go test
clean:
$(RM) $(TARGET)
Now, to test and compile gecko
we can just run the command make
.
What about subpaths?
Oftentimes Go CLI projects are structured with a directory of commands. CLI libraries, such as cobra, will look through this directory to find subcommands.
A typical CLI project layout will look something like this:
$ tree
.
├── Gopkg.lock
├── Gopkg.toml
├── LICENSE
├── cmd
│ ├── cmd.go
│ └── cmd_test.go
└── main.go
1 directory, 6 files
Because we won’t be compiling main.go
in the same directory as the cmd/test_*
files, we will need to make it available via PATH
:
Makefile
TARGET=main
BUILDPATH=.bin
all: clean build test
build:
mkdir -p $(BUILDPATH)
go build -o $(BUILDPATH)/$(TARGET)
test:
PATH=$(BUILDPATH):$PATH go test cmd
clean:
$(RM) $(BUILDPATH)/$(TARGET)
Now our test file can call the CLI application directly with exec.Command("main", "cmd")
and not have to worry about its location.