LoginSignup
7
3

More than 1 year has passed since last update.

Go言語 テスト用のモックを自動生成する (GoMock)

Posted at

はじめに

Go言語を用いた開発においては、コードの品質担保や予期せぬ修正の影響による不具合検知などのために、標準機能の go test で実行できる単体テストをあわせて書くことが多いと思います。

単体テスト実装に際しては、並列実行で短時間にテストを終わらせるためだったり、GitHub Actions や CircleCI のような限定的な環境で実行できるよう、ファイルの入出力や外部のAPIやDBサーバへの接続を行う処理などを、モックで代用して記述することが多いと思います。

インターフェイス定義などからモック処理を自動で生成してくれるツール
GoMock の導入と 生成したモックを用いたテストの書き方の紹介です

前提

以下の環境、知識を前提とします
- go 1.16以降 Go Modules を利用
- Go言語の基礎知識
- go test の基礎知識

テスト対象のサンプルコード

※以下サンプルコードではプロジェクトのリポジトリURLを example.com/gotest (go mod init example.com/gotestを実行した)と仮定しています。適宜読み替えてください。

entity/foo.go
package entity

// Foo entity
type Foo struct {
    ID    int64
    Value string
}
repository/foo.go
package repository

import (
    "example.com/gotest/entity"
)

type FooRepository interface {
    FindByID(id int64) (*entity.Foo, error)
    Insert(foo *entity.Foo) error
    Update(foo *entity.Foo) error
}

実装は省略しています

usecase/foo.go
package usecase

import (
    "example.com/gotest/entity"
    "example.com/gotest/repository"
)

type fooUseCase struct {
    fooRepo repository.FooRepository
}

func (p *fooUseCase) Save(id int64, value string) (*entity.Foo, error) {
    foo, err := p.fooRepo.FindByID(id)
    if err != nil {
        // 何らかのエラー
        return nil, err
    }
    if foo != nil {
        // 既存
        foo.Value = value
        if err := p.fooRepo.Update(foo); err != nil {
            return nil, err
        }
    } else {
        // 新規作成
        foo = &entity.Foo{ID: id, Value: value}
        if err := p.fooRepo.Insert(foo); err != nil {
            return nil, err
        }
    }
    // 再取得
    return p.fooRepo.FindByID(id)
}

モック生成

mockgen CLIのインストール (初回のみ)

モックを生成するCLIをインストールします

go install github.com/golang/mock/mockgen@v1.6.0

モックコードの生成

以下の例では repository.FooRepository インターフェイスの実装のモックを生成させます

mockgen \
  -source repository/foo.go \
  -destination repository/mock/foo.go \
  -package mock

-sourceでモック化対象のソースファイルを指定
-destinationで生成したモックを保存するソースファイル名、
-packageで生成するモックのパッケージ名を指定

テストの実装

usecase/foo_test.go
package usecase

import (
    "reflect"
    "testing"

    "github.com/golang/mock/gomock"

    "example.com/gotest/entity"
    "example.com/gotest/repository/mock"
)

// TestFooUseCase_Save_NotExistsSuccess 新規保存のテストケース
func TestFooUseCase_Save_NotExistsSuccess(t *testing.T) {
    // 期待値
    testID := int64(123)
    testValue := "hoge"
    expectedSaved := &entity.Foo{
        ID:    testID,
        Value: testValue,
    }

    // モックセット
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    fooRepoMock := mock.NewMockFooRepository(ctrl)
    fooRepoMock.EXPECT().FindByID(gomock.Eq(testID)).Return(nil, nil).Times(1)
    fooRepoMock.EXPECT().Insert(gomock.Eq(expectedSaved)).Return(nil)
    fooRepoMock.EXPECT().FindByID(gomock.Eq(testID)).Return(expectedSaved, nil).Times(1)

    // モックを注入して作成
    u := &fooUseCase{fooRepo: fooRepoMock}
    // 実行
    actualSaved, err := u.Save(testID, testValue)
    // 結果確認
    if err != nil {
        t.Errorf("予期せぬエラーが発生 %v", err)
    }
    if !reflect.DeepEqual(actualSaved, expectedSaved) {
        t.Errorf("保存結果が期待値と異なる\n期待:%+v\n実際:%+v", expectedSaved, actualSaved)
    }
}

補足

import

gomockパッケージ "github.com/golang/mock/gomock" と
上記で生成したモックパッケージ "example.com/gotest/repository/mock" をimport

モック関数生成と期待値のセット

各テスト関数では gomock.NewController を呼び出してモックコントローラを初期化し、
戻り値(ctrl)を各モックの New関数 NewMockモック化オブジェクト名(ctrl)に渡してやる必要があります。

呼び出されることが期待されるすべてのモックの関数の引数の期待値(マッチ条件)と戻り値を設定する必要があります。
モックオブジェクト.EXPECT().呼び出される関数(1つ目の引数の期待値, 2つ目の引数の期待値...).Return(モック関数が返す戻り値)

.Times(回数)は呼び出される回数を限定、

同じモック関数が複数呼び出されるようなケースでは、
モック関数の呼び出し順序を厳密に指定するgomock.InOrder関数で囲んだり、
.Return(戻り値)の代わりに.DoAndReturn(戻り値を返す関数)で引数に応じた戻り値を返すことなどができます

テスト実行

$ go test -v --cover ./...

=== RUN   TestFooUseCase_Save_NotExistsSuccess
--- PASS: TestFooUseCase_Save_NotExistsSuccess (0.00s)
PASS
coverage: 50.0% of statements
ok      example.com/gotest/usecase     0.264s  coverage: 50.0% of statements

無事に実行できました

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3