Jun 18, 2014 - 2 minutes

Go-lang: mocking exec.Command using interfaces

This is a short example showing how to use an interface to ease testing, and how to use an interface with running shell commands / other programs and providing mock output.

Source on Github

Here is our main file that actually runs the commands and prints out “hello”.

 1 package main
 2 
 3 import (
 4 	"fmt"
 5 	"os/exec"
 6 )
 7 
 8 // first argument is the command, like cat or echo,
 9 // the second is the list of args to pass to it
10 type Runner interface {
11 	Run(string, ...string) ([]byte, error)
12 }
13 
14 type RealRunner struct{}
15 
16 var runner Runner
17 
18 // the real runner for the actual program, actually execs the command
19 func (r RealRunner) Run(command string, args ...string) ([]byte, error) {
20 	out, err := exec.Command(command, args...).CombinedOutput()
21 	return out, err
22 }
23 
24 func Hello() string {
25 	out, err := runner.Run("echo", "hello")
26 	if err != nil {
27 		panic(err)
28 	}
29 	return string(out)
30 }
31 
32 func main() {
33 	runner = RealRunner{}
34 	fmt.Println(Hello())
35 }

Here is our test file. We start by defining our TestRunner type and implementing the Run(...) interface for it.

This function builds up a command to run the current test file and run the TestHelperProcess function passing along all the args you originally sent. This lets you do things like return different output for different commands you want to run.

The TestHelperProcess function exits when run in the context of the test file, but runs when specified in the files arguments.

 1 package main
 2 
 3 import (
 4 	"fmt"
 5 	"os"
 6 	"os/exec"
 7 	"testing"
 8 )
 9 
10 type TestRunner struct{}
11 
12 func (r TestRunner) Run(command string, args ...string) ([]byte, error) {
13 	cs := []string{"-test.run=TestHelperProcess", "--"}
14 	cs = append(cs, args...)
15 	cmd := exec.Command(os.Args[0], cs...)
16 	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
17 	out, err := cmd.CombinedOutput()
18 	return out, err
19 }
20 
21 func TestHello(t *testing.T) {
22 	runner = TestRunner{}
23 	out := Hello()
24 	if out == "testing helper process" {
25 		t.Logf("out was eq to %s", string(out))
26 	}
27 }
28 
29 func TestHelperProcess(*testing.T) {
30 	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
31 		return
32 	}
33 	defer os.Exit(0)
34 	fmt.Println("testing helper process")
35 }

Hopefully this helps someone else! I had a hard time finding some good, short examples on the internet that combined both interfaces and mocking like this.

More examples from os/exec/exec_test.go

comments powered by Disqus