📝

io.Reader や io.Writer を mock したい。

2022/04/24に公開
1

io.Reader / io.Writer ってなにさ?

Go でバイト列の読み出しや書き込みを行う際に必ずと言っていいほど使われるのが io.Readerio.Writerです。例えばライブラリを実装している時にバイト列の読み出しや書き込むが発生する場合には、これらのインターフェースを実装した任意のオブジェクトを受け取れるようにしておき、実際の読み出しや書き込みはそのオブジェクトに移譲するという実装がスタンダードです。なぜこのやり方が最高なのかということについては手前味噌ながら「io.Readerをすこれ」という Qiita の記事に是非目を通してみてください。

テストのめんどくささ

io.Readerio.Writer を介した実装は実際にコードを書いていたりユーザが使用する段階ではとても便利なのですが、一点だけ面倒なことがあります。それがテストです。文字列を受け取ったり返したりする実装と比べるとどうしても「エラーハンドリング」が入ってくるのでテストのカバレッジをきちんと確保するためには様々な場面でエラーを上げる必要がありますが、これを簡単に実現できる仕組みが標準ライブラリにはありません。最初は都度必要に応じて書くのがいいのではないかと考えていたのですが、実はものすごくシンプルな仕組みで実現できてしまうということに気がついたので実装してみました。

iomock でらくらく mock

iomockio.Readerio.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.Bufferstrings.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.Readerio.Writer ですが、それを使った開発工程が面倒くさいのはつらいですよね。iomock によって誰かの Go ライフをちょっとだけ豊かにできたのなら嬉しいです!

Discussion

Kotone/NanoKotone/Nano

NewReader/NewWriterは冗長だった(実際はReader/Writerへの型変換だけでいける)のでなくしました。