discordgoでE2Eのテストコードを書く
あいさつ
みなさん、こんにちは!マグロです。
4月になり、新年度が始まりました。新しい環境でのスタートはいかがでしょうか?
僕は新年度になってもdiscordbotを作っています。
今回は、discordgoでテストコードを書く方法について解説します。
テストコードとは
プログラムの動作を確認するためのコードのことです。
テストコードを書くことで、プログラムの動作が正しいかどうかを確認することができます。
まあ要するに書けばお得です。
Goでのテストコードの書き方
知見がある人はスキップしてください。
Goでのテストコードの書き方
Goでは、テストコードを書くための標準パッケージが用意されています。
testファイルの作成
テストコードは、テスト対象のコードと同じディレクトリに_test.go
という名前のファイルを作成します。
例えば、add.go
というファイルがある場合、テストコードはadd_test.go
という名前のファイルに書きます。
テスト関数の作成
テストコードは、Test
から始まる関数を作成します。
package add
import "testing"
func TestAdd(t *testing.T) {
if add(1, 2) != 3 {
t.Error("add(1, 2) should be 3")
}
}
func add(a, b int) int {
return a + b
}
TestAdd
関数は、add(1, 2)
が3
を返すかどうかを確認しています。
テストの実行
テストコードは、以下のコマンドで実行します。
$ go test
interfaceを使った結合テスト
Goにはinterfaceというものがあります。
簡単に言うと、中身の関数(メゾット)が同じであれば、異なる構造体でもひとくくりにして扱えます。
例えば、以下のようなコードがあるとします。
package main
import "fmt"
type math struct{}
func (m *math) adder(a, b int) int {
return a + b
}
func addCmd(m *math) int {
a := 1
b := 2
return m.adder(a, b)
}
func main() {
m := math{}
a := addCmd(&m)
fmt.Println(a)
}
上記の処理はただの足し算ですが、データベースや外部APIとの通信など、複雑な処理を行う場合、テストを書くことが難しくなります。
そこで、interfaceを使ってテストを行います。
package main
import (
"testing"
)
type math struct{}
func (m *math) adder(a, b int) int {
return a + b
}
type mathMock struct {
adderFunc func(a, b int) int
}
func (m *mathMock) adder(a, b int) int {
return m.adderFunc(a, b)
}
type mathFunc interface {
adder(a, b int) int
}
var (
_ mathFunc = (*mathMock)(nil)
_ mathFunc = (*math)(nil)
)
func addCmd(m mathFunc) int {
a := 1
b := 2
return m.adder(a, b)
}
func TestAdd(t *testing.T) {
m := &mathMock{
adderFunc: func(a, b int) int {
return 3
},
}
if addCmd(m) != 3 {
t.Error("addCmd should be 3")
}
}
上記のコードでは、math
構造体の代わりにmathMock
構造体を使っています。
mathMock
構造体は、adderFunc
という関数を持っており、コード内のように戻り値を指定することができます。
データベース関連の操作であれば、データベースを使用せずにテストを行うことができます。
このように、interfaceを使うことで、結合テストを行いやすくすることができます。
discordgoでのテストコードの書き方
ディレクトリ構成です。以下のGitHubリポジトリを参考にしています。
ここを見ながら進めていきます。
├── bot // DiscordBotを動かすためのディレクトリ
│ ├── cogs // DiscordBotのコグ
| ├── commands // スラッシュコマンド
│ ├── config // 環境変数設定ファイル
│ └── main.go
該当項目でも書きますが、今回行うテストするものの仕様は以下の通りです。
-
cog
-
onMessageCreate
メゾット- Botからのメッセージが送信された場合、何も返さない。
-
ping
が送信された場合、pong
を返す。 -
!hello
が含まれるメッセージが送信された場合、何も返さない。 - 上記以外の場合、
Hello, World!
を返す。
-
-
commands
-
command_hanlder
- 登録するのはpingコマンド。
- 同じコマンド名は登録できない。
- 登録した場合、構造体にデータが格納される。(ローカルでの参照が可能)
- 登録に失敗した場合、構造体にデータは格納されない。
-
getCommand
関数でコマンドを取得する。 -
commandDelete
関数でコマンドを削除する。
-
ping
- 実行されると
pong
を返す。 -
response
オプションを適応すると、そのオプションで指定した文字のメッセージを返す。
- 実行されると
-
共通事項
当たり前ですが、単体テストはdiscordgo側が行っています。
そのため、今回書くのは結合テストです。
結合テストのため、discordgoの機能を使っている部分をモック化します。
client
もモック化していますが、今回は使用しません。
*discordgo.Session
は引数に指定せず、mock.Session
というinterfaceを使用します。
discordgoでは、discordgo.Session.AddHandler
でイベントを受け取った際の処理を登録できます。
しかし、引数と戻り値の指定がある都合上、戻り値にerror
などを指定できず、正しい挙動をしたかどうかの確認ができません。
なぜ戻り値を指定できないのか
s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
// ここに処理を書く
})
上記のコードは、s
とm
を引数に取りますが、戻り値と引数をこれ以上追加するとエラーが発生します。
どのような関数なのかでイベントを判別しているためです。
結合テストでは、正しく処理できたか、想定通りのエラーが発生するかを確認する必要があります。
最低限error
を返さないと、テストで確認できるものがありません。
cogsのテストコードの書き方
cogsは、discordbotの機能を追加するためのディレクトリです。
cog_handler.go
というファイルに、コグの登録処理を記述します。
cogHandler
という構造体があります。
cogHandler
にはonMessageCreate
というメゾットがあり、discordgo.MessageCreate
イベントを受け取った際の処理を記述します。
とりあえず注目してほしいところは、onMessageCreate
メゾットです。
func (h *cogHandler) onMessageCreate(s *discordgo.Session, vs *discordgo.MessageCreate) {
ctx := context.Background()
_, err := onMessageCreateFunc(ctx, h.client, s, s.State, vs)
if err != nil {
slog.ErrorContext(ctx, "OnMessageCreate Error", "Error:", err.Error())
}
}
本来ここでは、discordgo.MessageCreate
イベントを受け取り、処理を行います。
しかし、テストコードを書くためには、error
を返す必要があります。
そこで本来の処理を書き込んだonMessageCreateFunc
メゾットを作成します。
onMessageCreate
メゾットは、onMessageCreateFunc
メゾットを呼び出し、error
を返します。
ついでに何を送信したのかも返すように*discordgo.Message
も返します。
func onMessageCreate(
ctx context.Context,
client *http.Client,
s mock.Session,
state *discordgo.State,
vs *discordgo.MessageCreate,
) (*discordgo.Message, error) {
// 本来の処理
}
これで正しい処理が行われたか確認できます。
というわけで、テストコードを書いていきます。
サンプルの仕様は以下の通りです。
テストするonMessageCreateFunc
は以下のような仕様です。
- Botからのメッセージが送信された場合、何も返さない。
-
ping
が送信された場合、pong
を返す。 -
!hello
が含まれるメッセージが送信された場合、何も返さない。 - 上記以外の場合、
Hello, World!
を返す。
&mock.SessionMock
を引数に渡すことで、モックを使用してテストを行います。
実際に送信はされず、送信されたとしてレスポンスを返すようにしています。
テストを実行して、正しく処理が行われているか確認してみましょう。
commandsのテストコードの書き方
登録のテスト
commandsは、discordbotのスラッシュコマンドを追加するためのディレクトリです。
command_handler.go
というファイルに、スラッシュコマンドの登録処理を記述します。
commandRegister
関数でコマンドを登録し、handler.commands
に格納します。
commandRemove
関数でコマンドを削除し、handler.commands
内のデータも削除します。
getCommand
関数でhandler.commands
からコマンドを取得します。
登録はDiscordとの通信以外にも、handler.commands
に格納することで、どのコマンドが登録されているかを確認できます。
コマンドが登録されているかどうかを確認するために、テストコードを書いていきます。
確認しているのは以下の項目です。
- pingコマンドが登録されているか。
- 同じコマンド名は登録できないか。
- 登録に失敗した場合、構造体にデータが格納されていないか。
- コマンドが削除されているか。
- 未登録の場合、エラーが発生するか。
cogsと同様に&mock.SessionMock
を引数に渡すことで、モックを使用してテストを行います。
同じくテストを実行して、正しく処理が行われているか確認してみましょう。
実行のテスト
スラッシュコマンドの処理は以下のように記述します。
Optionはスラッシュコマンドのオプション(discord.go公式のサンプルコード参照)、Executorは実行する関数を指定します。
Executorに格納する関数は以下のようなものです。
スラッシュコマンドに対する返信とエラーを返します。
func(s mock.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error)
ping
は以下のような仕様です。
- 実行されると
pong
を返す。 -
response
オプションを適応すると、そのオプションで指定した文字のメッセージを返す。
また、discordgoの仕様上、複数のスラッシュコマンドが登録されている場合、コマンド名関係なくすべて実行してしまいます。
そのため、コマンド名が違う場合のテストケースも含めて書きます。
これも&mock.SessionMock
を引数に渡すことで、モックを使用してテストを行います。
テストを実行して、正しく処理が行われているか確認してみましょう。
まとめ
今回は、discordgoでテストコードを書く方法について解説しました。
動作確認と挙動の理解が深まるので、おすすめです。
今回はあくまでも一例で、他にも様々なテストコードの書き方があります。
用途に応じて適切なテストコードを書いていきましょう。
Discussion