あぼぼーぼ・ぼーぼぼ

のんびり生きたい

slack-go/slackを使ったAPI通信をモックしてテストする

Go言語でSlack APIを使ったシステムを作る場合https://pkg.go.dev/github.com/slack-go/slackが便利なのですが、テストどう書くと良いかな〜と迷いました。

github.com

先に結論

結論から言うとhttps://pkg.go.dev/github.com/slack-go/slack/slacktestパッケージを利用するとこのように記述することができます。

// ①
//go:embed testdata/sample.json
var sampleJSON []byte

func Test(t *testing.T) {
    // ②
    ts := slacktest.NewTestServer(func(c slacktest.Customize) {
        c.Handle("/conversations.history", func(w http.ResponseWriter, r *http.Request) {
            w.Write(sampleJSON)
        })
    })
    ts.Start()

    // ③
    s := slack.New("testToken", slack.OptionAPIURL(ts.GetAPIURL()))
    resp, err := s.GetConversationHistory(nil)
// 省略...

以下3つのポイントを説明します。

① Slack APIのレスポンスを模したJSONファイルを読み込む

Slackのテストには直接関係ありませんが、Slack APIのレスポンスを模したJSONファイルを用意して、Go1.16から使えるgo:embedで読み込みます。

② slacktestパッケージのNewTestServer関数を使ってモックサーバーを立ち上げる

slacktestパッケージをうまく使うことでラクにテストが書けました。

NewTestServer関数の実装は以下のようになっています。ポイントはbinder型の可変長引数を、for rangeでループしながら実行しているところです。またその下には固定でいくつかのAPIをモックしているのが見えますね。今回はここに無い/conversations.historyAPIをモックします。

func NewTestServer(custom ...binder) *Server {
    serverChans := newMessageChannels()

    channels := &serverChannels{}
    groups := &serverGroups{}
    s := &Server{
        registered:           map[string]struct{}{},
        mux:                  http.NewServeMux(),
        seenInboundMessages:  &messageCollection{},
        seenOutboundMessages: &messageCollection{},
    }

    for _, c := range custom {
        c(s)
    }

    s.Handle("/conversations.info", s.conversationsInfoHandler)
    s.Handle("/ws", s.wsHandler)
    s.Handle("/rtm.start", rtmStartHandler)
    s.Handle("/rtm.connect", RTMConnectHandler)
    s.Handle("/chat.postMessage", s.postMessageHandler)
    s.Handle("/conversations.create", createConversationHandler)
// 省略

binder型は何なのか見にいくと、Customizeインタフェースを引数に持つ関数だということが分かります。

// Customize the server's responses.
type Customize interface {
    Handle(pattern string, handler http.HandlerFunc)
}

type binder func(Customize)

なので、モックしたいパスと、そのレスポンスをbinder型に合うようにNewTestServer関数に引数として渡してあげることで、モックサーバーが作れます。

ts := slacktest.NewTestServer(func(c slacktest.Customize) {
    c.Handle("/conversations.history", func(w http.ResponseWriter, r *http.Request) {
        w.Write(sampleJSON)
    })
})
ts.Start()

③ モックサーバーのURLを使ってslack.Clientを作成する

slackパッケージのNew関数でslack.Clientを作成する際に、引数にオプションを指定します。

slack.New関数は以下のような実装になっています。options引数はOptionの可変長引数で、for-rangeで実行していることから何かの関数だということがわかります。

// New builds a slack client from the provided token and options.
func New(token string, options ...Option) *Client {
    s := &Client{
        token:      token,
        endpoint:   APIURL,
        httpclient: &http.Client{},
        log:        log.New(os.Stderr, "slack-go/slack", log.LstdFlags|log.Lshortfile),
    }

    for _, opt := range options {
        opt(s)
    }

    return s
}

Optionは以下のように定義されており、Clientのポインタを引数に持つ関数です。その下のコードを見ていくと、Optionを返すユースケースごとの関数がいくつか用意されています。今回は指定したURLのレスポンスをモックしたいので、OptionAPIURL関数を利用します。

// Option defines an option for a Client
type Option func(*Client)

// OptionHTTPClient - provide a custom http client to the slack client.
func OptionHTTPClient(client httpClient) func(*Client) {
    return func(c *Client) {
        c.httpclient = client
    }
}

// OptionDebug enable debugging for the client
func OptionDebug(b bool) func(*Client) {
    return func(c *Client) {
        c.debug = b
    }
}

// OptionLog set logging for client.
func OptionLog(l logger) func(*Client) {
    return func(c *Client) {
        c.log = internalLog{logger: l}
    }
}

// OptionAPIURL set the url for the client. only useful for testing.
func OptionAPIURL(u string) func(*Client) {
    return func(c *Client) { c.endpoint = u }
}

モックサーバーのURLはts.GetAPIURL()で取得できるので、OptionAPIURL関数の引数に渡してあげます。

s := slack.New("testToken", slack.OptionAPIURL(ts.GetAPIURL()))

あとは、モックサーバーで指定したパスに該当するslack.Clientのメソッドを呼ぶことで、モックされたレスポンスを受け取ることができます。

s.GetConversationHistory(params) //paramsは別途定義が必要です