Ruby では I/O を伴うテストには StringIO
オブジェクトを使うことが多いとおもいます。
これは文字列を IO
オブジェクトと同じインターフェイスで操作するためのものです。
Golang でも似たようなことをやるにあたって、調べてみたところ標準ライブラリの bytes.Buffer
が使えそうだということがわかりました。
ここでは I/O を伴うプログラムをテスタブルに実装し、bytes.Buffer
でユニットテストを書くところまでを紹介します。
なお、私は Golang については初心者なので、より良いやり方等あれば是非教えてください。
実装するプログラム
全く実用的ではないですが、標準入力を受け取り、それを 2 度出力するプログラムを書いてみます。
以下のように動作します。
$ echo foo | ./double
foo
foo
実装してみる
こんな感じのプログラムを用意してみました。
package main
import (
"io"
"io/ioutil"
"os"
)
func main() {
err := Double(os.Stdin, os.Stdout)
if err != nil {
panic(err)
}
}
func Double(stdin io.Reader, stdout io.Writer) error {
buf, err := ioutil.ReadAll(stdin)
if err != nil {
return err
}
stdout.Write(buf)
stdout.Write(buf)
return nil
}
このプログラムを実装するにあたってのポイントは以下の通りです。
テスト対象のロジックを関数に切り出す
ユニットテストをする上では当たり前ですが、まずはテスト対象を関数に切り出さなければなりません。
このプログラムにおいては Double()
関数をテストできるように切り出しています。
構造体ではなくインターフェイスの型に依存させる
Double()
関数は引数として標準入力と標準出力の構造体へのポインタを受け取ります。
この場合は、例えば以下のように書くこともできます。
func Double(stdin *os.File, stdout *os.File) error {
}
ですが、ここではそうではなく、io.Reader
や io.Writer
を指定しています。
こうすることで、本物の os.File
構造体を用意せずとも、io.Reader
や io.Writer
インターフェイスを実装した何かであれば何でも使うことができます。
io.Reader
は Read()
メソッドを、io.Writer
は Write()
メソッドをそれぞれひとつだけ持ったシンプルなインターフェイスです。
ユニットテストを書いてみる
次に、上記の実装をユニットテストするコードを書いてみます。
package main
import (
"bytes"
"testing"
)
func TestDouble(t *testing.T) {
stdin := bytes.NewBufferString("foo\n")
stdout := new(bytes.Buffer)
err := Double(stdin, stdout)
if err != nil {
t.Fatal("failed to call Double(): %s", err)
}
expected := []byte("foo\nfoo\n")
if bytes.Compare(expected, stdout.Bytes()) != 0 {
t.Fatal("not matched")
}
}
このテストコードのポイントは以下の通りです。
ファイル (標準入出力) の代わりに bytes.Buffer
を使用する
stdin
と stdout
はそれぞれ書き方は違うものの、いずれも bytes.Buffer
構造体を生成しています。
stdin
には "foo\n"
という文字列を持ったものとして生成し、stdout
については空の状態で初期化しています。
stdin := bytes.NewBufferString("foo\n")
stdout := new(bytes.Buffer)
これは Ruby でいえば以下のような感じになるでしょうか。
stdin = StringIO.new("foo\n")
stdout = StringIO.new
書き込まれたバイト列を bytes.Buffer.Bytes()
で読み込んで比較する
bytes.Buffer
に書き込まれたバイト列は Bytes()
メソッドで読み出すことができます。
これを期待されるバイト列 expected
と比較することでアサーションを行っています。
なお、bytes.Buffer
は String()
メソッドも持っており、これは string
として読み出すことができます。
その場合は expected
も string
で指定する必要があります。
Bytes()
と String()
のどちらを使うべきか、については私自身はよくわかっていないのですが、[]byte
と string
の変換にかかるオーバーヘッドを考えると、Bytes()
がいいのではないかと思っています。
とはいえ、今回実装した程度であればどちらでも誤差程度しか違わないでしょう。