My External Storage

Sep 9, 2018 - 4 minute read - Comments - go

Goのtestingを理解する in 2018 - iotestサブパッケージ編 #go

この記事は以下の記事で触れなかったtesting/iotestについて触れる。

TL;DR

  • testing/iotestパッケージ
  • testing/iotestパッケージはio.Reader/io.Writerのテスト用のラッパーを提供する
  • エッジケースな挙動あるいはエラーを戻すラッパーと入出力をフックするラッパーが定義されている
  • 入出力周りのテストヘルパーを書くときの参考にもなる

func DataErrReader(r io.Reader) io.Reader

DataErrReaderRead(p []byte) (n int, err error)メソッドを呼んだとき最後にn != 0, err = io.EOFを返すio.Readerオブジェクトを戻す。

	orign := []byte("Hello\nbyte.Reader\n")
	type want struct {
		n         int
		buf       string
		wantError error
	}
	tests := []struct {
		subject string
		r       io.Reader
		size    int
		wants   []want
	}{
		{
			"DataErrReader",
			iotest.DataErrReader(bytes.NewReader(orign)),
			5,
			[]want{
				{5, "Hello", nil},
				{5, "\nbyte", nil},
				{5, ".Read", nil},
				{3, "er\n\x00\x00", io.EOF}, // return with io.EOF
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.subject, func(t *testing.T) {
			for _, want := range tt.wants {
				buf := make([]byte, tt.size)
				size, err := tt.r.Read(buf)
				if size != want.n {
					t.Fatalf("want %d, but got = %d\n", want.n, size)
				}
				if string(buf) != want.buf {
					t.Fatalf("want %#v, but got = %#v\n", want.buf, string(buf))
				}
				if err != want.wantError {
					t.Fatalf("want io.EOF, but got = %#v\n", err)
				}
			}
		})
	}

通常、io.Readerインターフェースの実装には、err == io.EOFのときn = 0かつその前の読み込みで全てのデータの読み込みが終了していることを想定するだろう。
DataErrReaderでラップしたio.Readerのオブジェクトは読み込みが終わった時、io.EOFと読み込んだデータを同時に返す。
「そのような実装を想定しないといけないことがあるのか?」というと、http.Getがそのような動きをするらしい。

func HalfReader(r io.Reader) io.Reader

https://golang.org/pkg/testing/iotest/#HalfReader

	orign := []byte("Hello\nbyte.Reader\n")
	type want struct {
		// DataErrReaderのサンプルコードと同じなので省略
	}
	tests := []struct {
		// DataErrReaderのサンプルコードと同じなので省略
	}{
		{
			"HalfReader",
			iotest.HalfReader(bytes.NewReader(orign)),
			5,
			[]want{
			  // len(5)のbufferでReadしても、半分の3バイトしか読み込んでくれない
				{3, "Hel\x00\x00", nil},
				{3, "lo\n\x00\x00", nil},
				{3, "byt\x00\x00", nil},
				{3, "e.R\x00\x00", nil},
				{3, "ead\x00\x00", nil},
				{3, "er\n\x00\x00", nil},
				{0, "\x00\x00\x00\x00\x00", io.EOF},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.subject, func(t *testing.T) {
			// DataErrReaderのサンプルコードと同じなので省略
		})
	}
}

func OneByteReader(r io.Reader) io.Reader

OneByteReaderはその名の通り、常に1バイトしか読みこまないio.Readerを返す。

	orign := []byte("Hello\nbyte.Reader\n")
	type want struct {
		// DataErrReaderのサンプルコードと同じなので省略
	}
	tests := []struct {
		// DataErrReaderのサンプルコードと同じなので省略
	}{
		{
			"OneByteReader",
			iotest.OneByteReader(bytes.NewReader(orign)),
			5,
			[]want{
				// 1バイトしか読み込まない
				{1, "H\x00\x00\x00\x00", nil},
				{1, "e\x00\x00\x00\x00", nil},
				{1, "l\x00\x00\x00\x00", nil},
				{1, "l\x00\x00\x00\x00", nil},
				{1, "o\x00\x00\x00\x00", nil},
				{1, "\n\x00\x00\x00\x00", nil},
				{1, "b\x00\x00\x00\x00", nil},
				{1, "y\x00\x00\x00\x00", nil},
				{1, "t\x00\x00\x00\x00", nil},
				{1, "e\x00\x00\x00\x00", nil},
				{1, ".\x00\x00\x00\x00", nil},
				{1, "R\x00\x00\x00\x00", nil},
				{1, "e\x00\x00\x00\x00", nil},
				{1, "a\x00\x00\x00\x00", nil},
				{1, "d\x00\x00\x00\x00", nil},
				{1, "e\x00\x00\x00\x00", nil},
				{1, "r\x00\x00\x00\x00", nil},
				{1, "\n\x00\x00\x00\x00", nil},
				{0, "\x00\x00\x00\x00\x00", io.EOF},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.subject, func(t *testing.T) {
			// DataErrReaderのサンプルコードと同じなので省略
		})
	}

func TimeoutReader(r io.Reader) io.Reader

TimeoutReaderは二回目のRead呼び出し時にエラーが発生する。返されるエラーはiotestパッケージ内に定義されている。

	orign := []byte("Hello\nbyte.Reader\n")
	type want struct {
		// DataErrReaderのサンプルコードと同じなので省略
	}
	tests := []struct {
		// DataErrReaderのサンプルコードと同じなので省略
	}{
		{
			"TimeoutReader",
			iotest.TimeoutReader(bytes.NewReader(orign)),
			5,
			[]want{
				{5, "Hello", nil},
				{0, "\x00\x00\x00\x00\x00", iotest.ErrTimeout}, // ErrTimeout on the second read with no data.
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.subject, func(t *testing.T) {
			// DataErrReaderのサンプルコードと同じなので省略
		})
	}

func TruncateWriter(w io.Writer, n int64) io.Writer

TruncateWriterは第二引数で指定されたバイト数だけしかwに書き込こまないio.Writerオブジェクトを返す。 一定以上書き込めない出力先を生成するのに利用できる。

	func TestTruncateWriter(t *testing.T) {
	orign := []byte("Hello\nbyte.Reader\n")
	wants := []struct {
		buf string
		n   int
	}{
		{"Hel", 3},
		{"Hello\nby", 8},
	}

	for _, want := range wants {
		b := &bytes.Buffer{}
		w := iotest.TruncateWriter(b, int64(want.n))
		w.Write(orign)

		if got := b.Bytes(); string(got) != want.buf {
			t.Fatalf("want %#v, but got = %#v\n", want.buf, string(got))
		}
	}
}

func NewWriteLogger(prefix string, w io.Writer) io.Writer

NewWriteLogger/NewReadLoggerは書き込んだ(読み込んだ)結果をログ出力にフックするio.Writer(io.Reader)を返す。

たとえば、先ほどのTestTruncateWriterテストにNewWriteLoggerを挟むと、

		w = iotest.NewWriteLogger("Hook write", w)

io.Writerに書き込んだタイミングでその内容を16進数でログに出力する。

go test ./iotest -v -run TestTruncateWriter
=== RUN   TestTruncateWriter
2018/09/09 21:48:13 Hook write 48656c6c6f0a627974652e5265616465720a
2018/09/09 21:48:13 Hook write 48656c6c6f0a627974652e5265616465720a
--- PASS: TestTruncateWriter (0.00s)
PASS
ok  	github.com/budougumi0617/go-testing/iotest	0.009s

参考

関連

関連記事