Goのexec.Command()
をテスト(モック)したいときにどういう方法があるか調べたときのメモ
exec_test.go
の手法
この方法はexec.Command()
で実行するコマンド自体を差し替えて、テストコードのTestHelperProcess()
を実行した結果にしてしまうというものです。
この方法だとコマンドが実際に実行されたかどうかは検証できないため、モックと組み合わせてコマンドを実行したかどうかも検証できるようにしてみました。
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
インターフェースを定義してモックを生成します。
そしてテストコードを書きます。
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があります。
これを使う方法もためしてみました。
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
のモックを生成してテストコードを書きます。
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のライブラリを使うと短く簡単に書くことができます。