はじめに
TwitterAPIや形態素解析APIを使うサービスでテストをしたいときがあると思います。
また、レイヤードアーキテクチャ等で下層の処理を含めたテストしたいときがあると思います。
まさか、TwitterAPIを実際に叩いたりしたテストをしていませんか?
このような場合はテストモックを使ってテストを行うのが一般的です。
Goではモックライブラリを使わずに、自前でモックすることが多いのです。
テストと強気にでましたが、ここではユニットテストを指しています。
実装
以下のTwitterAPIクライアントをサービス層で利用するとします。
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クライアントをレシーバ、もしくは引数からインジェクションします。
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を叩くことになったでしょう。
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