やりたいこと
Goに限らずですがプログラムのテストコードを書く際、テスト対象の処理が他の処理に依存していることはよくあります。
しかし、ユニットテストとしては依存先に関しては出来るだけ触れずに対象の範囲に絞ったテストコードを簡潔に書きたいものです。
その際に依存先がinterfaceになっているとスタブのコードを用意してテストを楽にできま す。
サンプルコード
type Approver interface {
IsApproved() bool
}
type SendApprover struct {
hasCompanyApproved bool
hasTeamApproved bool
hasBossApproved bool
}
type Sender interface {
CanSend() bool
SetMessage(message string)
Send()
}
type MailSender struct {
approver Approver
email string
message string
}
func (a *SendApprover) IsApproved() bool {
return a.hasCompanyApproved && a.hasTeamApproved && a.hasBossApproved
}
func NewMailSender(approver Approver, email string) Sender {
return &MailSender{approver: approver, email: email}
}
func (s *MailSender) CanSend() bool {
return s.approver.IsApproved() && s.email != "" && s.message != ""
}
func (s *MailSender) SetMessage(message string) {
s.message = message
}
func (s *MailSender) Send() {
//
}
スタブに置き換えたい依存先の処理
サンプルなのでまとめて一箇所に書いていますが、Approver
の処理が今回スタブに置き換えたい依存先の処理になります。
今回は送信の許可を管理する処理の位置付けです。
type Approver interface {
IsApproved() bool
}
type SendApprover struct {
hasCompanyApproved bool
hasTeamApproved bool
hasBossApproved bool
}
func (a *SendApprover) IsApproved() bool {
return a.hasCompanyApproved && a.hasTeamApproved && a.hasBossApproved
}
テスト対象
Approver
に依存している送信処理としてSender
およびMailSender
を用意しました。
今回はスタブにしたいのはApprover
なので、Sender
インターフェイスは無くても大丈夫です。
type Sender interface {
CanSend() bool
SetMessage(message string)
Send()
}
type MailSender struct {
approver Approver
email string
message string
}
func NewMailSender(approver Approver, email string) Sender {
return &MailSender{approver: approver, email: email}
}
func (s *MailSender) CanSend() bool {
return s.approver.IsApproved() && s.email != "" && s.message != ""
}
func (s *MailSender) SetMessage(message string) {
s.message = message
}
func (s *MailSender) Send() {
//
}
処理の仮定を分かりやすくするためにSetMessage
メソッドとSend
メソッドを用意しましたが、テストとしては省略しています。
テストコード
解説用にCanSend
メソッドのテストコードを書いてみます。
type StubApprover struct {
isApproved bool
}
func (s *StubApprover) IsApproved() bool {
return s.isApproved
}
func TestMailSender_CanSend(t *testing.T) {
tests := []struct {
name string
approver Approver
email string
message string
want bool
}{
{
name: "未承認の場合は送信できないこと",
approver: &StubApprover{isApproved: false},
want: false,
},
{
name: "メールアドレスが空の場合は送信できないこと",
approver: &StubApprover{isApproved: true},
want: false,
},
{
name: "メッセージが空の場合は送信できないこと",
approver: &StubApprover{isApproved: true},
email: "hoge@example.co.jp",
want: false,
},
{
name: "承認済みでメールアドレスとメッセージがある場合は送信できること",
approver: &StubApprover{isApproved: true},
email: "hoge@example.co.jp",
message: "Hello, World!",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sender := NewMailSender(tt.approver, tt.email)
sender.SetMessage(tt.message)
got := sender.CanSend()
assert.Equal(t, tt.want, got)
})
}
}
スタブ
スタブ用の構造体を新たに定義して、Approver
インターフェイスを実装しています。
type StubApprover struct {
isApproved bool
}
func (s *StubApprover) IsApproved() bool {
return s.isApproved
}
構造体のオブジェクトを生成した際にフィールドでセットしたbool値をそのまま判定結果になるようにしています。
スタブを使ったテストケース
approver: &StubApprover{isApproved: false},
などと記述している箇所で実際にスタブを使っています。
{
name: "未承認の場合は送信できないこと",
approver: &StubApprover{isApproved: false},
want: false,
},
{
name: "承認済みでメールアドレスとメッセージがある場合は送信できること",
approver: &StubApprover{isApproved: true},
email: "hoge@example.co.jp",
message: "Hello, World!",
want: true,
},
ここでセットした値を使って処理が行われます。
func (s *MailSender) CanSend() bool {
return s.approver.IsApproved() && s.email != "" && s.message != ""
}
s.approver.IsApproved()
は下記の処理に置き換わっているので、本来ならSendApprover
の処理で行われる細かい制御を無視することができます。
func (s *StubApprover) IsApproved() bool {
return s.isApproved
}
スタブを使うメリット
- テストコードを書く際に依存先のデータ条件などを細かく配慮する必要が無くなる
- 楽にかける
- コード量を減らせる
- 実装ミスを減らせる
- 依存先の処理を再テストするような作りを避けられる
- 依存先の処理の変更にも強くなる
- 可読性が上がる
- 依存先の内部的な処理詳細を把握していなくても、期待する出力のパターンだけ意識して読める
モックなのスタブなのか問題
本筋ではないですが、余談を一つ。
「モック」と呼ぶか「スタブ」と呼ぶかは人によってブレがありますよね。
今回のような処理はスタブと呼ぶのがいいかなと思ってますが、ちょっと不安...。