💉
Goでテスタビリティを目的とした場合の最小限のDI設計
誰に対して向けた記事か
- Clean Architecture的な思想で依存関係を逆転させた綺麗な設計にするのはぶっちゃけ面倒くせえ
- でもテストのためにDIやmockはしなくちゃいけねえ
って人のための記事です。
何を解説する記事か
Goでテスタビリティを目的とした場合の最小限のDI設計を2つ紹介します。
- funcを引数に取る方法
- interfaceを使う方法
問題の例
以下のようなコードを書いたとします。
要はVery色んな処理
が終わったらSlack通知を行うアプリケーションです。
package main
import (
"fmt"
"github.com/slack-go/slack"
"os"
)
func main() {
c := slack.New(os.Getenv("SLACK_TOKEN"))
err := Very色んな処理()
msg, err := HandleErrorV1(c, err)
if err != nil {
panic(err)
}
fmt.Println(msg)
}
func HandleErrorV1(c *slack.Client, err error) (string, error) {
var msg string
if err == nil {
msg = "Very色んな処理が完了しました\n"
} else {
msg = fmt.Sprintf("エラーが発生しました: %v\n", err)
}
if _, _, err = c.PostMessage("#general", slack.MsgOptionText(msg, true)); err != nil {
return "", fmt.Errorf("slackへのメッセージ送信に失敗しました: %v", err)
}
return msg, nil
}
func Very色んな処理() error {
// 実際にはめっちゃ色んな処理がここに書かれてることにしてください
return nil
}
この中のHandleErrorV1
をテストしたいとします。
しかし、HandleErrorV1
はslack.Client
に依存しているため、テストを走らせるたびにSlackに通知が飛んでしまいます。
解決案 funcを引数に取る方法
「問題の例」の問題を解決するために、以下のようなコードを書くことができます。
package main
import (
"fmt"
"github.com/slack-go/slack"
"os"
)
func main() {
err := Very色んな処理()
msg, err := HandleErrorV2(err, PostMessageImpl) // ここで注入
if err != nil {
panic(err)
}
fmt.Println(msg)
}
// 引数のfuncにてDIっぽいことを実現する
func HandleErrorV2(err error, postMessage func(string, slack.MsgOption) (string, string, error)) (string, error) {
var msg string
if err == nil {
msg = "Very色んな処理が完了しました\n"
} else {
msg = fmt.Sprintf("エラーが発生しました: %v\n", err)
}
if _, _, err = postMessage("#general", slack.MsgOptionText(msg, true)); err != nil {
return "", fmt.Errorf("slackへのメッセージ送信に失敗しました: %v", err)
}
return msg, nil
}
func PostMessageImpl(channel string, msgOption slack.MsgOption) (string, string, error) {
return slack.New(os.Getenv("SLACK_TOKEN")).PostMessage(channel, msgOption)
}
func Very色んな処理() error {
// 実際にはめっちゃ色んな処理がここに書かれてることにしてください
return nil
}
以下の特徴があります。
-
HandleErrorV2
はslack.Client
に依存せず、PostMessage
という関数を引数に取ることでDIを実現している -
HandleErrorV2
の使用側でPostMessageImpl
を注入する
これにより、HandleErrorV2
のテストを以下のように書くことができます。
package main
import (
"errors"
"github.com/slack-go/slack"
"github.com/stretchr/testify/assert"
"testing"
)
func TestHandleErrorV2(t *testing.T) {
mockPostMessage := func(channel string, msgOption slack.MsgOption) (string, string, error) {
// 実際の通知をさせず、何も起こさない
return "", "", nil
}
msg, err := HandleErrorV2(errors.New("何かしらのエラー"), mockPostMessage)
assert.Nil(t, err)
assert.Equal(t, "エラーが発生しました: 何かしらのエラー\n", msg)
}
解決案 interfaceを使う方法
また、interfaceを使う方法も紹介します。
Goであればできる設計であり、最小限の定義だけで実現できます。
package main
import (
"fmt"
"github.com/slack-go/slack"
"os"
)
func main() {
c := slack.New(os.Getenv("SLACK_TOKEN"))
err := Very色んな処理()
msg, err := HandleErrorV3(err, c)
if err != nil {
panic(err)
}
fmt.Println(msg)
}
func HandleErrorV3(err error, slackClient slackClientInterface) (string, error) {
var msg string
if err == nil {
msg = "Very色んな処理が完了しました\n"
} else {
msg = fmt.Sprintf("エラーが発生しました: %v\n", err)
}
if _, _, err = slackClient.PostMessage("#general", slack.MsgOptionText(msg, true)); err != nil {
return "", fmt.Errorf("slackへのメッセージ送信に失敗しました: %v", err)
}
return msg, nil
}
type slackClientInterface interface {
// 以下のシグネチャをinterfaceとして定義しただけ
// https://pkg.go.dev/github.com/slack-go/slack#Client.PostMessage
// Goは明示的なimplementsが不要なので、ライブラリに合うinterfaceをこちらで勝手に定義して使える
PostMessage(channelID string, options ...slack.MsgOption) (string, string, error)
}
func Very色んな処理() error {
// 実際にはめっちゃ色んな処理がここに書かれてることにしてください
return nil
}
以下の特徴があります。
-
HandleErrorV3
はslack.Client
に依存せず、slackClientInterface
という独自定義のinterfaceを引数に取ることでDIを実現している -
slackClientInterface
はslack.Client
のPostMessage
メソッドのシグネチャをそのまま定義しているだけ -
slack.Client
はslackClientInterface
を実装したことになってる(Goは明示的なimplementsが不要 -
HandleErrorV3
の使用側でslackClientInterface
を実装した型であるslack.Client
を注入する
これにより、HandleErrorV3
のテストを以下のように書くことができます。
package main
import (
"errors"
"github.com/slack-go/slack"
"github.com/stretchr/testify/assert"
"testing"
)
type mockSlackClient struct{}
func (m *mockSlackClient) PostMessage(channelID string, options ...slack.MsgOption) (string, string, error) {
// 実際の通知をさせず、何も起こさない
return "", "", nil
}
func TestHandleErrorV2(t *testing.T) {
msg, err := HandleErrorV3(errors.New("何かしらのエラー"), &mockSlackClient{})
assert.Nil(t, err)
assert.Equal(t, "エラーが発生しました: 何かしらのエラー\n", msg)
}
Discussion