Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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つの処理を別関数で分離します。

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は、以下のメソッドを満たしていることを条件としています。

getObject() (*s3.GetObjectOutput, error)

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

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

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

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

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

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

終わりに

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

こちらにサンプルコードを公開しています。

flatnyat
フリーランスエンジニア AWS Scala Go Rust PHP Node.js 専門はサーバーサイドです
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away