AWS SDK for Go を使用しているコードのユニットテスト

ユニットテストはテスト対象コードに外部依存がない状態で実施するのが理想的です。
外部依存はデータベース参照やWeb APIへのリクエストなど、別コンポーネントとのやりとりを行う箇所で発生しますが、Goではインターフェースを利用してその箇所の実装を差し替えることで、外部依存を除いてテストを実施することができます。

AWS SDK for Goは、AWSの各種リソースをGoのプログラムから扱うためのライブラリです。
内部的にはAWS APIを利用しているため、外部依存を除いてテストをするためには実装を気をつける必要があります。このエントリではその具体的な方法について紹介します。

## サンプルコード

以下はステータスが`running`であるEC2インスタンスのインスタンスIDを表示するプログラムです。
AWS SDK for Goを利用してインスタンス一覧を取得する処理は、main関数から呼び出される`DescribeRunningInstances`で行っています。

```main.go
package main

import (
	"fmt"
	"os"

	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ec2"
)

func DescribeRunningInstances() ([]string, error) {
	var runningInstances []string

	sess := session.Must(session.NewSession())
	svc := ec2.New(sess)

	input := &ec2.DescribeInstancesInput{}
	result, err := svc.DescribeInstances(input)
	if err != nil {
		return runningInstances, err
	}

	for _, r := range result.Reservations {
		if *r.Instances[0].State.Name == "running" {
			runningInstances = append(runningInstances, *r.Instances[0].InstanceId)
		}
	}

	return runningInstances, nil
}

func main() {
	instances, err := DescribeRunningInstances()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	for _, instance := range instances {
		fmt.Println(instance)
	}

	os.Exit(0)
}
```

このプログラムのコアロジックである`DescribeRunningInstances`をテストしたいのですが、そのままですと`svc.DescribeInstances(input)`が外部依存なので、これを何とかする必要があります。

## インターフェースを利用する

AWS SDK for Goでは、各種サービスクライアントのモックを作成するためのインターフェースを提供されています。
サンプルプログラムでは"github.com/aws/aws-sdk-go/service/ec2"を利用していますので、その場合は"github.com/aws/aws-sdk-go/service/ec2/ec2iface"が利用できます。

"github.com/aws/aws-sdk-go/service/ec2/ec2iface"では`EC2API`というインターフェースが定義されています。
`ec2.EC2`は`ec2iface.EC2API`インターフェースを実装しているので、サンプルプログラムで`ec2iface.EC2API`を利用することでテストのときに実装を差し替えられるようにしたいと思います。

```main.go
package main

import (
	"fmt"
	"os"

	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"

	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ec2"
)

func DescribeRunningInstances(svc ec2iface.EC2API) ([]string, error) {
	var runningInstances []string

	input := &ec2.DescribeInstancesInput{}
	result, err := svc.DescribeInstances(input)
	if err != nil {
		return runningInstances, err
	}

	for _, r := range result.Reservations {
		if *r.Instances[0].State.Name == "running" {
			runningInstances = append(runningInstances, *r.Instances[0].InstanceId)
		}
	}

	return runningInstances, nil
}

func main() {
	sess := session.Must(session.NewSession())
	svc := ec2.New(sess)

	instances, err := DescribeRunningInstances(svc)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	for _, instance := range instances {
		fmt.Println(instance)
	}

	os.Exit(0)
}
```

まず、実際のEC2サービスクライアントの作成はmain関数で行います。

```go
	sess := session.Must(session.NewSession())
	svc := ec2.New(sess)
```

`DescribeRunningInstances`では引数で`ec2iface.EC2API`型の変数を受け取るようにして、それをEC2サービスクライアントとして利用します。

```
func DescribeRunningInstances(svc ec2iface.EC2API) ([]string, error) {
	var runningInstances []string

	input := &ec2.DescribeInstancesInput{}
	result, err := svc.DescribeInstances(input)
```

これにより、テストのときは`ec2iface.EC2API`を実装したモッククライアントを引数で渡せるようになり、実装の差し替えが可能になります。

## テスト

```main_test.go
package demo2

import (
	"reflect"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
)

type mockEC2Svc struct {
	ec2iface.EC2API
}

func (m *mockEC2Svc) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
	return &ec2.DescribeInstancesOutput{
		Reservations: []*ec2.Reservation{
			{Instances: []*ec2.Instance{{InstanceId: aws.String("i-aaaaaa"), State: &ec2.InstanceState{Name: aws.String("running")}}}},
			{Instances: []*ec2.Instance{{InstanceId: aws.String("i-bbbbbb"), State: &ec2.InstanceState{Name: aws.String("running")}}}},
			{Instances: []*ec2.Instance{{InstanceId: aws.String("i-cccccc"), State: &ec2.InstanceState{Name: aws.String("running")}}}},
			{Instances: []*ec2.Instance{{InstanceId: aws.String("i-dddddd"), State: &ec2.InstanceState{Name: aws.String("stopped")}}}},
		},
	}, nil
}

func TestDescribeRunningInstances(t *testing.T) {
	svc := &mockEC2Svc{}
	runningInstance, err := DescribeRunningInstances(svc)
	if err != nil {
		t.Error(err)
	}

	expected := []string{
		"i-aaaaaa",
		"i-bbbbbb",
		"i-cccccc",
	}

	if !reflect.DeepEqual(expected, runningInstance) {
		t.Errorf("expected %q to eq %q", expected, runningInstance)
	}
}
```

`ec2iface.EC2API`を埋め込んだ構造体`mockEC2Svc`を定義します。

```
type mockEC2Svc struct {
	ec2iface.EC2API
}
```

これで`mockEC2Svc`は`ec2iface.EC2API`インターフェースを実装したことになりますので、`DescribeRunningInstances`の引数に渡すことができます。

```
	svc := &mockEC2Svc{}
	runningInstance, err := DescribeRunningInstances(svc)
```

`mockEC2Svc`を引数に渡したことで、`DescribeRunningInstances`の中で呼び出されている`svc.DescribeInstances`の実装はモックに置き換えられています。
ただし、この時点ではまだ具体的な実装をしていませんので、`svc.DescribeInstances`を呼び出しても何もしない状態です。これではテストができませんので、`mockEC2Svc`の`DescribeInstances`メソッドにテスト用の実装を書いていきます。
今回は4インスタンスのうち3インスタンスのステータスが`running`である状態を想定したレスポンスを返します。

```
func (m *mockEC2Svc) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
	return &ec2.DescribeInstancesOutput{
		Reservations: []*ec2.Reservation{
			{Instances: []*ec2.Instance{{InstanceId: aws.String("i-aaaaaa"), State: &ec2.InstanceState{Name: aws.String("running")}}}},
			{Instances: []*ec2.Instance{{InstanceId: aws.String("i-bbbbbb"), State: &ec2.InstanceState{Name: aws.String("running")}}}},
			{Instances: []*ec2.Instance{{InstanceId: aws.String("i-cccccc"), State: &ec2.InstanceState{Name: aws.String("running")}}}},
			{Instances: []*ec2.Instance{{InstanceId: aws.String("i-dddddd"), State: &ec2.InstanceState{Name: aws.String("stopped")}}}},
		},
	}, nil
}
```

あとは通常どおりにテストを書けばOKです。`DescribeRunningInstances`はstring型のスライスを返すので、期待される結果を`expected`に定義して`reflect.DeepEqual`で`DescribeRunningInstances`で返された値と比較します。

```
	expected := []string{
		"i-aaaaaa",
		"i-bbbbbb",
		"i-cccccc",
	}

	if !reflect.DeepEqual(expected, runningInstance) {
		t.Errorf("expected %q to eq %q", expected, runningInstance)
	}
```

## まとめ

外部依存の箇所はあらかじめインターフェースを利用した実装にしておくのがよさそうです。

## 参考文献

- https://docs.aws.amazon.com/ja_jp/sdk-for-go/v1/developer-guide/welcome.html
- https://godoc.org/github.com/aws/aws-sdk-go
- https://godoc.org/github.com/aws/aws-sdk-go/service/ec2/ec2iface
- https://deeeet.com/writing/2016/10/25/go-interface-testing/
- https://qiita.com/tenntenn/items/bba69d84a1e0b67c4db8