LoginSignup
11
8

More than 5 years have passed since last update.

Go で外部コマンドをユニットテストする

Last updated at Posted at 2014-11-15

envjson というコマンドを作る上で得られた知見として、外部コマンドをユニットテストする方法について紹介します。

テスティングフレームワークとしてはとりあえず標準の testing を使っていますが、これは他の何でもいいと思います。
(私自身はこれ以外のフレームワークはまだ使ったことがないんですが)

TL;DR

os/execexec.Command() でコマンドを実行し、標準入出力として bytes.Buffer を差し込んでおくことでテストが可能。

exec.Cmd()

標準ライブラリ "os/exec" に含まれる exec.Command() 関数は外部コマンドを実行するための関数です。
より正確には、exec.Command() 関数で exec.Cmd 構造体を返し、Run() メソッドを実行することで外部コマンドが実行されます。

実行する前に exec.Cmd 構造体が持つプロパティをいろいろと差し替えることで、コマンドの入出力を差し替えることが可能です。

テスト対象のコマンド

パイプから入力を受け取り、フィルタするいかにも UNIX 的なコマンドラインツールのことを考えます。

例えば、標準入力を受け取って、それを 2 回出力する double コマンドについて考えてみます。

main.go
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" を渡し、標準出力として実行元プロセスの標準出力を渡しています。

main_test.go
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" が出力される」といったユニットテストができます。

main_test.go
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.BufferString() メソッドで中身を取り出し、期待される出力と比較することでテストを行っています。

bytes.Buffer は内部的には []byte でデータを保持しますが、このように string として取り出すことができます。
Bytes() メソッドで []byte を受け取ることもできるので、必要に応じて使い分けると良いでしょう。

11
8
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
11
8