datsukan blog
🦹🏼‍♂️

Goでintefaceを使ってスタブによる楽なテストコードを作る

やりたいこと

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
}

スタブを使うメリット

  • テストコードを書く際に依存先のデータ条件などを細かく配慮する必要が無くなる
    • 楽にかける
    • コード量を減らせる
    • 実装ミスを減らせる
  • 依存先の処理を再テストするような作りを避けられる
    • 依存先の処理の変更にも強くなる
  • 可読性が上がる
    • 依存先の内部的な処理詳細を把握していなくても、期待する出力のパターンだけ意識して読める

モックなのスタブなのか問題

本筋ではないですが、余談を一つ。
「モック」と呼ぶか「スタブ」と呼ぶかは人によってブレがありますよね。
今回のような処理はスタブと呼ぶのがいいかなと思ってますが、ちょっと不安...。

コメント
 
URLをコピーしました
Profile picture
datsukan

24歳。埼玉県在住。東京都のSaaS企業でバックエンドエンジニアとして勤務しています。

© 2022 datsukan