Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
53
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

I/O を伴うテストには bytes.Buffer が便利

Ruby では I/O を伴うテストには StringIO オブジェクトを使うことが多いとおもいます。
これは文字列を IO オブジェクトと同じインターフェイスで操作するためのものです。

Golang でも似たようなことをやるにあたって、調べてみたところ標準ライブラリの bytes.Buffer が使えそうだということがわかりました。
ここでは I/O を伴うプログラムをテスタブルに実装し、bytes.Buffer でユニットテストを書くところまでを紹介します。

なお、私は Golang については初心者なので、より良いやり方等あれば是非教えてください。

実装するプログラム

全く実用的ではないですが、標準入力を受け取り、それを 2 度出力するプログラムを書いてみます。

以下のように動作します。

$ echo foo | ./double
foo
foo

実装してみる

こんな感じのプログラムを用意してみました。

main.go
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.Readerio.Writer を指定しています。

こうすることで、本物の os.File 構造体を用意せずとも、io.Readerio.Writer インターフェイスを実装した何かであれば何でも使うことができます。
io.ReaderRead() メソッドを、io.WriterWrite() メソッドをそれぞれひとつだけ持ったシンプルなインターフェイスです。

ユニットテストを書いてみる

次に、上記の実装をユニットテストするコードを書いてみます。

main_test.go
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 を使用する

stdinstdout はそれぞれ書き方は違うものの、いずれも 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.BufferString() メソッドも持っており、これは string として読み出すことができます。
その場合は expectedstring で指定する必要があります。

Bytes()String() のどちらを使うべきか、については私自身はよくわかっていないのですが、[]bytestring の変換にかかるオーバーヘッドを考えると、Bytes() がいいのではないかと思っています。
とはいえ、今回実装した程度であればどちらでも誤差程度しか違わないでしょう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
53
Help us understand the problem. What are the problem?