【Go】ユニットテストのMock、Table driven Testとか


下記のコースのアウトプットになります。
Unit testing for Go developers | Udemy

参考にした記事
Goでテストを書く(テストの実装パターン集) - Qiita

Golang のテスト

実行コマンド

go test ./...

詳細を知りたい場合
go test -v ./...

特定のファイルを実行する
go test app/domain/service/survey_test.go

メッセージ

TODO

Table driven Test

Table Driven Testとは、それぞれの行が入力と期待する出力を含んだテストケースになっているテーブルを作り、それをループして実行する形式のテストです。
DMMにおけるユーザーレビュー基盤の変革(Goのテスト技法編) - DMM inside

テスト対象のコードです。
下記からは、コースで使用したソースを使っていきます。

package calculator

import "errors"

type DiscountCalculator struct {
    minimumPurchaseAmount int
    discountAmount        int
}

func NewDiscountCalculator(minimumPurchaseAmount int, discountAmount int) (*DiscountCalculator, error) {
    if minimumPurchaseAmount == 0 {
        return &DiscountCalculator{}, errors.New("minimum purchase amount could not be zero")
    }
    return &DiscountCalculator{
        minimumPurchaseAmount: minimumPurchaseAmount,
        discountAmount:        discountAmount,
    }, nil
}

func (c *DiscountCalculator) Calculate(purchaseAmount int) int {
    if purchaseAmount > c.minimumPurchaseAmount {
        multiplier := purchaseAmount / c.minimumPurchaseAmount
        return purchaseAmount - (c.discountAmount * multiplier)
    }

    return purchaseAmount
}

下記でもわかる通り、テストデータが非常にわかりやすいです。

package calculator

import "testing"

// testingの宣言は下記のような感じ
func TestDiscountCalculator(t *testing.T) {
    type testCase struct {
        name                  string
        minimumPurchaseAmount int
        discount              int
        purchaseAmount        int
        exepectedAmount       int
    }
    // テストケーズをまとめているところ
    // テストケースが一目でわかりやすい
    testCases := []testCase{
        {name: "should apply 20", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 150, exepectedAmount: 130},
        {name: "should apply 40", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 200, exepectedAmount: 160},
        {name: "should apply 60", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 350, exepectedAmount: 290},
        {name: "should not apply", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 50, exepectedAmount: 50},
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            calculator := NewDiscountCalculator(tc.minimumPurchaseAmount, tc.discount)
            amount := calculator.Calculate(tc.purchaseAmount)

            if amount != tc.exepectedAmount {
                t.Errorf("exepected %v, got %v", tc.exepectedAmount, amount)
            }
        })
    }
}
// 実行結果
=== RUN   TestDiscountCalculator
=== RUN   TestDiscountCalculator/should_apply_20
=== RUN   TestDiscountCalculator/should_apply_40
=== RUN   TestDiscountCalculator/should_apply_60
=== RUN   TestDiscountCalculator/should_not_apply
--- PASS: TestDiscountCalculator (0.00s)
    --- PASS: TestDiscountCalculator/should_apply_20 (0.00s)
    --- PASS: TestDiscountCalculator/should_apply_40 (0.00s)
    --- PASS: TestDiscountCalculator/should_apply_60 (0.00s)
    --- PASS: TestDiscountCalculator/should_not_apply (0.00s)

golangでテストを書くならテーブルドリブンテスト - Qiita

例外処理

TODO

Assert

assert.Equal(t, tc.exepectedAmount, amount)

詳しくは下記で。
GitHub - stretchr/testify: A toolkit with common assertions and mocks that plays nicely with the standard library

Mock(モック)

Mockは、必要なテスト条件を容易に作ることができます。
Mockでユニットテストを簡単にしよう! | GMOアドパートナーズグループ TECH BLOG byGMO

Golangでは、下記記事が一番わかりやすかったです。
testify/mockでgolangのテストを書く - Qiita

// Mock の宣言
type DiscountRepositoryMock struct {
    mock.Mock
}

// interface 宣言
type Discount interface {
    FindCurrentDiscount() int
}

// Mockを使ったメソッドの呼び出し
func (drm DiscountRepositoryMock) FindCurrentDiscount() int {
    // 条件にあったReturn設定値を返す
    args := drm.Called()
    return args.Int(0)
}

for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            // Mockを初期化
            discountRepositoryMock := DiscountRepositoryMock{}
            // 使用するメソッドと返り値を設定する          
            discountRepositoryMock.On("FindCurrentDiscount").Return(tc.discount)

            calculator, err := NewDiscountCalculator(tc.minimumPurchaseAmount, discountRepositoryMock)
            if err != nil {
                // FailNow + log
                t.Fatalf("could not instantiate the calculator %s", err.Error())
            }
            amount := calculator.Calculate(tc.purchaseAmount)

            assert.Equal(t, tc.exepectedAmount, amount)
        })
    }