2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

exec.Command()をモックする

Posted at

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

exec_test.goの手法

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

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

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インターフェースを定義してモックを生成します。
そしてテストコードを書きます。

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があります。
これを使う方法もためしてみました。

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のモックを生成してテストコードを書きます。

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のライブラリを使うと短く簡単に書くことができます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?