Goでテストを書くために、まずinterfaceを設計するということ

## Goでテストコードを書くことになった場合に

あなたが、勤務先で上長から「テストコードを書いてください」と指示されたとき、
テストが書ける設計になっているでしょうか。

この記事では、**S3からファイルを取得して読み取る**というユースケースに対して
テストを書くことについて考えていきます。

## テストが書きづらいケース

```main.go
var awssess = newSession()
var s3Client = newS3Client()

const defaultRegion = "ap-northeast-1"

func newSession() *session.Session {
	return session.Must(session.NewSessionWithOptions(session.Options{
		SharedConfigState: session.SharedConfigEnable,
	}))
}

func newS3Client() *s3.S3 {
	return s3.New(awssess, &aws.Config{
		Region: aws.String(defaultRegion),
	})
}

func readObject(bucket, key string) ([]byte, error) {
	obj, err := s3Client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return nil, err
	}

	defer obj.Body.Close()
	res, err := ioutil.ReadAll(obj.Body)
	if err != nil {
		return nil, err
	}

	return res, nil
}

func main() {
	res, err := readObject("{Your Bucket}", "{Your Key}")
	if err != nil {
		log.Println(err)
	}

	log.Println(string(res))

	return
}
```

S3バケットに存在するファイルを`log.Println()`により表示しています。
`readObject()`に対してテストコードを書きます。
しかし`readObject()`はS3に直接アクセスする処理が含まれており、外部処理に完全に依存しています。
このままではテストを実行するごとにS3へのアクセスを実行しなくてはならず、現実的ではありません。

ここでまず考えるべきことは、**モック**を作るということです。

## 関数を分ける

まず、現状の`readObject()`の問題点は以下の2点が同じ関数に存在することにあります。

- S3からファイルの取得
- ファイルの読み取り

そこで、この2つの処理を別関数で分離します。

```go
func getObject(bucket, key string) (*s3.GetObjectOutput, error) {
	obj, err := s3Client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return nil, err
	}

	return obj, nil
}

func readObject(bucket, key string) ([]byte, error) {
	obj, err := getObject(bucket, key)
	if err != nil {
		return nil, err
	}

	defer obj.Body.Close()
	res, err := ioutil.ReadAll(obj.Body)
	if err != nil {
		return nil, err
	}

	return res, nil
}
```

これで分離ができました。
しかし、このままでは前述のS3アクセスへの依存は解決できていません。

ここで、**interface**の出番です。

## interfaceの実装

結論から述べると、モック化したい関数は`interface`の関数として実装するとよいです。
当記事では、`getObject()`を`interface`の関数とします。

そうすることで、`getObject()`の中の処理を外から渡すことができます。
正常時はS3へのアクセスを渡し、テスト時はモックを渡すことができるわけです。

以下が`interface`を実装した全体コードです。

```main.go
package main

import (
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"io/ioutil"
	"log"
)

var awssess = newSession()
var s3Client = newS3Client()

const defaultRegion = "ap-northeast-1"

func newSession() *session.Session {
	return session.Must(session.NewSessionWithOptions(session.Options{
		SharedConfigState: session.SharedConfigEnable,
	}))
}

func newS3Client() *s3.S3 {
	return s3.New(awssess, &aws.Config{
		Region: aws.String(defaultRegion),
	})
}

type objectGetterInterface interface {
	getObject() (*s3.GetObjectOutput, error)
}

type objectGetter struct {
	Bucket string
	Key    string
}

func newObjectGetter(bucket, key string) *objectGetter {
	return &objectGetter{
		Bucket: bucket,
		Key:    key,
	}
}

func (getter *objectGetter) getObject() (*s3.GetObjectOutput, error) {
	obj, err := s3Client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(getter.Bucket),
		Key:    aws.String(getter.Key),
	})
	if err != nil {
		return nil, err
	}

	return obj, nil
}

func readObject(t objectGetterInterface) ([]byte, error) {
	obj, err := t.getObject()
	if err != nil {
		return nil, err
	}

	defer obj.Body.Close()
	res, err := ioutil.ReadAll(obj.Body)
	if err != nil {
		return nil, err
	}

	return res, nil
}

func main() {
	t := newObjectGetter("{Your Bucket}", "{Your Key}")
	res, err := readObject(t)
	if err != nil {
		log.Println(err)
	}

	log.Println(string(res))

	return
}
```

`readObject()`の引数に注目してください。
`objectGetterInterface`を引数に渡しています。

また、`getObject()`は`objectGetter`構造体をレシーバとし、メソッド化しています。

`objectGetterInterface`は、以下のメソッドを満たしていることを条件としています。

```go
getObject() (*s3.GetObjectOutput, error)
```

つまり、`objectGetter`構造体は`objectGetterInterface`の条件を満たしているということになります。

では、モックのメソッドについても考えていきます。
同じように、`getObject() (*s3.GetObjectOutput, error)`を満たすように実装してみましょう。

```go
type objectGetterMock struct{}

func (m objectGetterMock) getObject() (*s3.GetObjectOutput, error) {
	b := ioutil.NopCloser(strings.NewReader("hoge"))
	return &s3.GetObjectOutput{
		Body: b,
	}, nil
}
```

`hoge`という文字列が記録されているファイルが格納されているという想定です。

これで、`objectGetterMock`構造体も同様に`objectGetterInterface`の条件を満たすことができました。

では実際のテストコードをみてみましょう。

## テストコード

```main_test.go
package main

import (
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
	"io/ioutil"
	"strings"
	"testing"
)

type testSuite struct {
	suite.Suite
	service *objectGetter
}

func (s *testSuite) SetUpTest() {
	s.service.Bucket = "dummy"
	s.service.Key = "dummy"
}

func TestExecution(t *testing.T) {
	suite.Run(t, new(testSuite))
}

type objectGetterMock struct{}

func (m objectGetterMock) getObject() (*s3.GetObjectOutput, error) {
	b := ioutil.NopCloser(strings.NewReader("hoge"))
	return &s3.GetObjectOutput{
		Body: b,
	}, nil
}

func (s *testSuite) Test() {
	mock := objectGetterMock{}
	res, _ := readObject(mock)
	assert.Equal(s.T(), "hoge", string(res))
}

```

以下に注目してください。

```go
func (s *testSuite) Test() {
    mock := objectGetterMock{}
    res, _ := readObject(mock)
    assert.Equal(s.T(), "hoge", string(res))
}
```

`objectGetterMock`を`readObject()`に渡しています。
そのため、テストでは**S3からファイルの取得**について気にする必要はなく、**ファイルの読み取り**にフォーカスしたテストを書くことができました。

## 終わりに

interfaceを設計することでテストコードが非常に書きやすくなります。
ぜひ、テストコードを書くことで品質の高いプロダクトを目指してください。