テストのためのIO抽象

先日のGoken vol.14でJxckさんから良いことを聞いたのでメモに残す。

定義

何はともかく定義を。

type rwcMock struct {
    bytes.Buffer
    Closed bool
}

func (m *rwcMock) Close() error {
    m.Closed = true
    return nil
}

Goは、ある構造体の埋め込むことでその構造体のように振る舞え、そのオブジェクトに対してメソッドが定義されていたらインターフェースを満たすという機能がある。 bytes.Bufferの定義は、

```go:buffer.go type Buffer struct { // ... }

func (b *Buffer) Read(p []byte) (n int, err error) { // ... }

func (b *Buffer) Write(p []byte) (n int, err error) { // ... }

となり、これはio.ReadWriterインターフェースを満たす。

```go:io.go
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

そしてrwcMockはio.ReadWriteCloserインターフェースを満たす。

```go:io.go type Closer interface { Close() error }

type ReadWriteCloser interface { Reader Writer Closer }

## いかに使えるのか

ここでnet.Connとos.Fileの定義の抜粋を記す。

```go:net.go
type Conn interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    // ...
}

```go:os.go type File struct { // ... }

func (f *File) Read(b []byte) (n int, err error) { // ... }

func (f *File) Write(p []byte) (n int, err error) { // ... }

func (f *File) Close() error { // ... } ```

一目見て分かるように、これらのオブジェクトはio.ReadWriteCloserインターフェースを満たす。

つまり入出力でこれらのオブジェクトを使う箇所で型をio.ReadWriteCloserにしておけば、テストダブルとしてrwcMockを使えることを意味する。

Pythonにおけるfileオブジェクトの代わりにStringIOオブジェクトを渡して、内容を検査するのと同様なことがGoでもできる。

補足

POSIXではもっとプリミティブなファイルデスクリプタ、型でいうとuintptrまで抽象化することが可能なのだが、現状のGoはソケットのファイルデスクリプタを公開しておらず、内部的にはnetFDとして定義しているのだが、そとからはos.Fileでしか操作することが出来ない。

補足2

ここまで書いてから気付いた。net.Connインターフェースには定義されていないが、TCP/IPやUnixドメインソケットのConnオブジェクトにFile()メソッドが定義されていて、os.Fileオブジェクトが返るようだ。 つまり、ソケットでできることはファイルオブジェクトででもできる。 どちらにせよrwcMockのテストにおける有用性は変わらない。