envjson というコマンドを作る上で得られた知見として、外部コマンドをユニットテストする方法について紹介します。
テスティングフレームワークとしてはとりあえず標準の testing
を使っていますが、これは他の何でもいいと思います。
(私自身はこれ以外のフレームワークはまだ使ったことがないんですが)
TL;DR
os/exec
の exec.Command()
でコマンドを実行し、標準入出力として bytes.Buffer
を差し込んでおくことでテストが可能。
exec.Cmd()
標準ライブラリ "os/exec"
に含まれる exec.Command()
関数は外部コマンドを実行するための関数です。
より正確には、exec.Command()
関数で exec.Cmd
構造体を返し、Run()
メソッドを実行することで外部コマンドが実行されます。
実行する前に exec.Cmd
構造体が持つプロパティをいろいろと差し替えることで、コマンドの入出力を差し替えることが可能です。
テスト対象のコマンド
パイプから入力を受け取り、フィルタするいかにも UNIX 的なコマンドラインツールのことを考えます。
例えば、標準入力を受け取って、それを 2 回出力する double
コマンドについて考えてみます。
package main
import (
"io/ioutil"
"os"
)
func main() {
buf, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
os.Stdout.Write(buf)
os.Stdout.Write(buf)
}
今回はこれをテストするコードを用意します。
なお、今回は外部コマンドを直接実行することでテストするので、あらかじめ go build
して double
を executable としてコンパイルしておく必要があります。
$ go build -o double
$ echo foo | ./double
foo
foo
このように、標準入力として受け取った foo という文字列を 2 回出力しています。
標準入出力を差し替える
exec.Cmd
構造体は Stdio
, Stdout
, Stderr
というプロパティを持っており、それらにストリームを渡すことで、標準入出力を差し替えて実行することができます。
以下は標準入力として "foo\n"
を渡し、標準出力として実行元プロセスの標準出力を渡しています。
package main
import (
"bytes"
"os"
"os/exec"
"testing"
)
func TestDouble(t *testing.T) {
cmd := exec.Command("./double")
cmd.Stdin = bytes.NewBufferString("foo\n")
cmd.Stdout = os.Stdout
err := cmd.Run()
if err != nil {
t.Fatal("failed to execute ./double\n")
}
}
ここで go test
を実行すると以下のような出力が得られます。
$ go test
foo
foo
PASS
ok github.com/yuya-takeyama/double 0.054s
文字列 "foo\n"
を ./double
に渡すことでそれが 2 回、標準出力に出力されました。
ここで標準入力には bytes.Buffer
構造体を使用しています。
これは Ruby や Python における StringIO
オブジェクトのようなもので、ファイルのスタブとして使うのに便利です。
詳しくは I/O を伴うテストには bytes.Buffer が便利 を参照してください。
外部コマンドの出力をテストする
これをさらに一歩進めれば、「標準入力として "foo\n"
を渡せば標準出力に "foo\nfoo\n"
が出力される」といったユニットテストができます。
package main
import (
"bytes"
"os/exec"
"testing"
)
func TestDouble(t *testing.T) {
cmd := exec.Command("./double")
cmd.Stdin = bytes.NewBufferString("foo\n")
stdout := new(bytes.Buffer)
cmd.Stdout = stdout
err := cmd.Run()
if err != nil {
t.Fatal("failed to execute ./double\n")
}
if stdout.String() != "foo\nfoo\n" {
t.Fatal("not matched")
}
}
変数 stdout
に空の bytes.Buffer
構造体をセットしています。
cmd.Run()
によってコマンドを実行すると、その出力は stdout
変数に書き込まれることになります。
コマンドの実行後、bytes.Buffer
の String()
メソッドで中身を取り出し、期待される出力と比較することでテストを行っています。
bytes.Buffer
は内部的には []byte
でデータを保持しますが、このように string
として取り出すことができます。
Bytes()
メソッドで []byte
を受け取ることもできるので、必要に応じて使い分けると良いでしょう。