🤖

🤖

:gijutsu_burogu:

外部APIを実際に叩いたりしていませんか? GoでDIによるテストモック

この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

はじめに

TwitterAPIや形態素解析APIを使うサービスでテストをしたいときがあると思います。 また、レイヤードアーキテクチャ等で下層の処理を含めたテストしたいときがあると思います。 まさか、TwitterAPIを実際に叩いたりしたテストをしていませんか?

このような場合はテストモックを使ってテストを行うのが一般的です。 Goではモックライブラリを使わずに、自前でモックすることが多いのです。

テストと強気にでましたが、ここではユニットテストを指しています。

実装

以下のTwitterAPIクライアントをサービス層で利用するとします。

// twitter.go
package qiita_test_mock

import (
    "net/url"

    "github.com/ChimeraCoder/anaconda"
)

type ITwitterApiClient interface {
    PostTweet(string, url.Values) (anaconda.Tweet, error)
}

// Golangでよく見るanacondaのTwitterClient
// *anaconda.TwitterApiはPostTweet(string, url.Values) (anaconda.Tweet, error)を実装している
// したがって、TwitterApiClientはITwitterApiClientを満たしている
type TwitterApiClient *anaconda.TwitterApi

サービス層では以下のように書くことで、DIを実現することができます。 TwitterAPIクライアントをレシーバ、もしくは引数からインジェクションします。

// service.go
package qiita_test_mock

import "fmt"

// このInterfaceの宣言はなくてもPostHelloWorldは動作します
// ただ、ここもInterfaceとすることでService層をさらに上のUseCase層等へインジェクションすることができます
// そうすると、UseCase層はService層のモックを用いて実装やテストを行うことができます
type Service interface {
    PostHelloWorld(string) (string, error)
}

type ServiceImpl struct {
    TwitterApiClient ITwitterApiClient
}

// ITwitterApiClientを利用して、ツイートする
// 構造体の要素としてDIする(フィールドインジェクション)
func (s ServiceImpl) PostHelloWorld(name string) (string, error) {
    content := fmt.Sprintf("Hello World by %s", name)
    tweet, err := s.TwitterApiClient.PostTweet(content, nil)
    if err != nil {
        return "", err
    }
    return tweet.Text, nil
}

// 上と同様の処理を行う
// 引数としてDIする(あまり見たことはないし非推奨)
func PostHelloWorld(name string, client ITwitterApiClient) (string, error) {
    content := fmt.Sprintf("Hello World by %s", name)
    tweet, err := client.PostTweet(content, nil)
    if err != nil {
        return "", err
    }
    return tweet.Text, nil
}

テストは以下のように書くことができます。 TwitterAPIクライアントを外部からインジェクションしない場合は、テストでも実際のTwitterAPIを叩くことになったでしょう。

// service_test.go
package qiita_test_mock

import (
    "net/url"
    "testing"

    "github.com/ChimeraCoder/anaconda"
    "github.com/google/go-cmp/cmp"
)

type TwitterApiClientMock struct{}

// ITwitterApiClientを満たすようにPostTweetを実装する
func (m TwitterApiClientMock) PostTweet(content string, values url.Values) (anaconda.Tweet, error) {
    return anaconda.Tweet{Text: content}, nil
}

func TestPostHelloWorld(t *testing.T) {
    cases := []struct {
        input  string
        output string
    }{
        {
            input:  "Bob",
            output: "Hello World by Bob",
        },
        {
            input:  "Alice",
            output: "Hello World by Alice",
        },
        {
            input:  "Mike",
            output: "Hello World by Mike",
        },
    }
    // TwitterApiClientMockをインジェクションすることで、anacondaのAPIクライアントを用いない
    // したがって、実際にツイートすることなくテストを実行することができる
    serviceImpl := ServiceImpl{TwitterApiClient: TwitterApiClientMock{}}
    for _, tt := range cases {
        tweetContent, err := serviceImpl.PostHelloWorld(tt.input)
        if err != nil {
            t.Fatal(err)
        }
        if diff := cmp.Diff(tweetContent, tt.output); diff != "" {
            t.Errorf("Diff: (-got +want)\n%s", diff)
        }

        tweetContent, err = PostHelloWorld(tt.input, TwitterApiClientMock{})
        if err != nil {
            t.Fatal(err)
        }
        if diff := cmp.Diff(tweetContent, tt.output); diff != "" {
            t.Errorf("Diff: (-got +want)\n%s", diff)
        }
    }
}
# テスト結果
$ go test -v                                                                                                         
=== RUN   TestPostHelloWorld
--- PASS: TestPostHelloWorld (0.00s)
PASS
ok      github.com/kotaroooo0/for_output/qiita_test_mock    0.386s

ソースコードはこちら https://github.com/kotaroooo0/for_output/tree/master/qiita_test_mock

最後に

今回は外部APIを例に実装を紹介しました。 記事の途中でも紹介しましたが、この技法はレイヤードアーキテクチャなど多層のアーキテクチャの層をモックしたり、DBをモックしたりすることにも応用することができます。 あくまでもこれはユニットテストの話です。手でポチポチしたりして実際のAPIやDBを叩いたりするようにことも必要です。

参考

https://deeeet.com/writing/2016/10/25/go-interface-testing/
https://irof.hateblo.jp/entry/2017/04/16/222737