exec.Command()をモックする

Goの`exec.Command()`をテスト（モック）したいときにどういう方法があるか調べたときのメモ

## `exec_test.go`の手法

* https://golang.org/src/os/exec/exec_test.go
* https://npf.io/2015/06/testing-exec-command/

この方法は`exec.Command()`で実行するコマンド自体を差し替えて、テストコードの`TestHelperProcess()`を実行した結果にしてしまうというものです。

この方法だとコマンドが実際に実行されたかどうかは検証できないため、モックと組み合わせてコマンドを実行したかどうかも検証できるようにしてみました。

```go:exec/exec.go
package main

//go:generate mockgen -package=mock -destination=mock/exec.go -source=exec.go

import (
	"fmt"
	"os/exec"
)

func main() {
	r := CommandRunner{exec: CommandWrapper{}}
	fmt.Print(r.Run())
}

type CommandRunner struct {
	exec Exec
}

func (r CommandRunner) Run() string {
	output, _ := r.exec.Command("echo", "foo bar", "baz").CombinedOutput()

	return string(output)
}

type Exec interface {
	Command(name string, arg ...string) *exec.Cmd
}

type CommandWrapper struct{}

func (w CommandWrapper) Command(name string, arg ...string) *exec.Cmd {
	return exec.Command(name, arg...)
}
```

`Exec`インターフェースを定義してモックを生成します。
そしてテストコードを書きます。

```go:exec/exec_test.go
package main

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
	"testing"

	"github.com/golang/mock/gomock"

	"exec/mock"
)

func TestRun(t *testing.T) {
	c := gomock.NewController(t)
	defer c.Finish()

	mockExec := mock.NewMockExec(c)
	mockExec.
		EXPECT().
		Command("echo", "foo bar", "baz").
		Return(StubCommand("echo", "foo bar", "baz"))

	r := CommandRunner{exec: mockExec}
	if "foo bar baz" != r.Run() {
		t.Error("test run failed")
	}
}

func StubCommand(name string, arg ...string) *exec.Cmd {
	cs := []string{"-test.run=TestHelperProcess", "--", name}
	cs = append(cs, arg...)
	cmd := exec.Command(os.Args[0], cs...)
	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
	return cmd
}

func TestHelperProcess(t *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 := strings.Join(args, " ")
	switch cmd {
	case "echo foo bar baz":
		fmt.Fprint(os.Stdout, "foo bar baz")
	default:
		fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd)
		os.Exit(2)
	}
}
```

`StubCommand()`と`TestHelperProcess()`がコマンド差し替えにあたる部分です。
このテストコードでは`exec.Command()`の実行回数（1回）、引数、出力の検証ができています。

## k8s.io/utils/execを使う

（上の方法で実装してから気づいたのですが・・）
`exec.Command()`をテストできるライブラリ[k8s.io/utils/exec](https://godoc.org/k8s.io/utils/exec)があります。
これを使う方法もためしてみました。

```go:exec/exec.go
package main

//go:generate mockgen -package=mock -destination=mock/exec.go k8s.io/utils/exec Interface

import (
	"fmt"

	"k8s.io/utils/exec"
)

func main() {
	r := CommandRunner{exec: exec.New()}
	fmt.Print(r.Run())
}

type CommandRunner struct {
	exec exec.Interface
}

func (r CommandRunner) Run() string {
	output, _ := r.exec.Command("echo", "foo bar", "baz").CombinedOutput()

	return string(output)
}
```

インターフェース`exec.Interface`が用意されているのでそれを使います。
`exec.Interface`のモックを生成してテストコードを書きます。

```go:exec/exec_test.go
package main

import (
	"testing"

	"github.com/golang/mock/gomock"
	"k8s.io/utils/exec"
	fakeexec "k8s.io/utils/exec/testing"

	"exec/mock"
)

func TestRun(t *testing.T) {
	fakeCmd := &fakeexec.FakeCmd{
		CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{
			func() ([]byte, error) {
				return []byte("foo bar baz"), nil
			},
		},
	}
	c := gomock.NewController(t)
	defer c.Finish()

	mockExec := mock.NewMockInterface(c)
	mockExec.
		EXPECT().
		Command("echo", "foo bar", "baz").
		Return(fakeCmd)

	r := CommandRunner{exec: mockExec}
	if "foo bar baz" != r.Run() {
		t.Error("test run failed")
	}
}
```

テストコード用の`FakeCmd`などが用意されているため短く書くことができます。

## まとめ

`exec.Command()`をモックする方法を紹介しました。

はじめの`exec_test.go`の手法でもテストはできます。ただテスト実行時の動きがややわかりづらかったり、記述が少し長くなってしまうのが難点です。
[k8s.io/utils/exec](https://godoc.org/k8s.io/utils/exec)のライブラリを使うと短く簡単に書くことができます。