search
LoginSignup
13

More than 3 years have passed since last update.

posted at

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.EC2ec2iface.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関数で行います。

    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
}

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

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

mockEC2Svcを引数に渡したことで、DescribeRunningInstancesの中で呼び出されているsvc.DescribeInstancesの実装はモックに置き換えられています。
ただし、この時点ではまだ具体的な実装をしていませんので、svc.DescribeInstancesを呼び出しても何もしない状態です。これではテストができませんので、mockEC2SvcDescribeInstancesメソッドにテスト用の実装を書いていきます。
今回は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.DeepEqualDescribeRunningInstancesで返された値と比較します。

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

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

まとめ

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

参考文献

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
13