io.Reader や io.Writer を mock したい。
io.Reader
/ io.Writer
ってなにさ?
Go でバイト列の読み出しや書き込みを行う際に必ずと言っていいほど使われるのが io.Reader
と io.Writer
です。例えばライブラリを実装している時にバイト列の読み出しや書き込むが発生する場合には、これらのインターフェースを実装した任意のオブジェクトを受け取れるようにしておき、実際の読み出しや書き込みはそのオブジェクトに移譲するという実装がスタンダードです。なぜこのやり方が最高なのかということについては手前味噌ながら「io.Readerをすこれ」という Qiita の記事に是非目を通してみてください。
テストのめんどくささ
io.Reader
や io.Writer
を介した実装は実際にコードを書いていたりユーザが使用する段階ではとても便利なのですが、一点だけ面倒なことがあります。それがテストです。文字列を受け取ったり返したりする実装と比べるとどうしても「エラーハンドリング」が入ってくるのでテストのカバレッジをきちんと確保するためには様々な場面でエラーを上げる必要がありますが、これを簡単に実現できる仕組みが標準ライブラリにはありません。最初は都度必要に応じて書くのがいいのではないかと考えていたのですが、実はものすごくシンプルな仕組みで実現できてしまうということに気がついたので実装してみました。
iomock でらくらく mock
iomock は io.Reader
や io.Writer
といったインターフェースを簡単に mock するための小さなライブラリです。しかし小さいからと侮るなかれこの簡単な仕組みを使って実に表現力豊かな mock を書くことができます。例えば hello
という文字列が読まれたら io.ErrClosedPipe
エラーを返却する io.Reader
は以下のように mock することができます。
package main
import (
"fmt"
"io"
"github.com/ktnyt/iomock"
)
func main() {
buffer := []byte("hello")
r := iomock.Reader(func(p []byte) (int, error) {
n := copy(p, buffer)
if n == 0 {
return 0, io.ErrClosedPipe
}
buffer = buffer[n:]
return n, nil
})
p := make([]byte, 2)
fmt.Println(r.Read(p)) // 2 nil
fmt.Println(r.Read(p)) // 2 nil
fmt.Println(r.Read(p)) // 1 nil
fmt.Println(r.Read(p)) // 0 io: read/write on closed pipe
}
このように iomock.NewReader
に与える関数をスマートに作ることによって様々なテストケースに対応することができます。しかも型が io.Reader
なのでテーブルテストの構造体に通常のケースで使う bytes.Buffer
や strings.Builder
といったオブジェクトと同列に使うことができます。その際には以下のような薄い wrapper を書いてあげることで便利に使うことができるでしょう。
func newReaderMock(buffer []byte, err error) io.Reader {
return iomock.Reader(func(p []byte) (int, error) {
n := copy(p, buffer)
if n == 0 {
return 0, err
}
buffer = buffer[n:]
return n, nil
})
}
iomock.NewWriter
も基本的には同様で、 関数の型シグネチャも同じですが引数で受け取る p []byte
には書き込まれようとしているバイト列が渡されます。これを利用することで「n文字書き込まれたらエラーをあげる」「m回呼ばれたらエラーをあげる」「特定のバイト列でエラーをあげる」といったことが簡単に書けるようになります。
せっかく便利な io.Reader
や io.Writer
ですが、それを使った開発工程が面倒くさいのはつらいですよね。iomock によって誰かの Go ライフをちょっとだけ豊かにできたのなら嬉しいです!
Discussion
NewReader/NewWriterは冗長だった(実際はReader/Writerへの型変換だけでいける)のでなくしました。