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オブジェクトを戻す。

  • https://play.golang.org/p/e3eSeyirp_i

    	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

  • https://play.golang.org/p/7OBG8uUJu1B

    	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を返す。

  • https://play.golang.org/p/b-GVUag2eSN

    	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パッケージ内に定義されている。

  • https://play.golang.org/p/dnwZwG5NyzG

    	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 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

参考

関連

関連記事