lelelemon’s blog

カメの歩みでのんびり学んでいます。

【Go言語】gomock で外部へのアクセスをモック化して単体テストを書く

golang単体テストを勉強していて、外部リソースへのアクセスをモック化する方法を調べていたら gomock というのがあることを知り、試したサンプルです。

 

環境準備

mkdir service
mkdir repository
mkdir model
touch service/book_service.go
touch repository/book_repository.go
touch model/book.go

 

tree
.
├── go.mod
├── model
│   └── book.go
├── repository
│   └── book_repository.go
└── service
    └── book_service.go

 

サンプルは以下のような構成にしたいと思います。

  • book_service.go -> book 関連のビジネスロジックを提供。
  • book_repository.go -> book 関連の DB アクセス周りの処理を提供。
  • book.go -> book 構造体定義

 

単体テスト対象のコード

続いてテスト対象コードを用意していきます。

 

book.go
package model

type Book struct {
    Id    int
    Name  string
    Price int
}
book_repository.go
package repository

import "unittest/model"

type BookRepository interface {
    GetBookById(id int) model.Book
}

 

Repository にDB アクセス処理があると仮定して、これを後ほどモックします。

book_service.go
package service

import (
    "unittest/model"
    "unittest/repository"
)

type BookService struct {
    Repository repository.BookRepository
}

func (service *BookService) GetBookById(id int) model.Book {
    return service.Repository.GetBookById(id)
}

 

こちらの Service から、Repository の処理を呼び出します。

 

単体テストコード

mock の生成

まずは repository の mock を作成します。

gomock では、リンク先の README に書かれているように、mockgen コマンドで mock を自動生成できるようです。

 

それでは、先に作成した Repository の mock を作成してみます。

repository フォルダに移動して、下記のコマンドを実行します。

 

mockgen -source book_repository.go -destination mock/book_repository.go

 

すると、mock フォルダが生成され、その中に book_repository.go が生成されます。

.
├── book_repository.go
└── mock
    └── book_repository.go

 

生成された  book_repository.go の下記のimport 句でコンパイルエラーになっているので、

gomock "github.com/golang/mock/gomock"

 

unittest フォルダ直下で下記コマンドを実行して gomock の依存を追加します。

go mod tidy

 

単体テストコード

repository の mock が作れたので、続いて単体テストコードを書きます。

repository を使用する Service の単体テストを書いていきます。

 

package service

import (
    "testing"
    "unittest/model"
    mock_db "unittest/repository/mock"

    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/assert"
)

func TestGetBookById(t *testing.T) {
    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()

    expectedBook := model.Book{Id: 100, Name: "テストブック", Price: 1500}

    mockObj := mock_db.NewMockBookRepository(mockCtrl)
    mockObj.EXPECT().GetBookById(100).Return(expectedBook)

    service := BookService{Repository: mockObj}

    actualBook := service.GetBookById(100)

    assert.Equal(t, expectedBook, actualBook)
}

 

テストの検証を簡潔にするため、github.com/stretchr/testify/assert を使用しています。

コンパイルエラーになると思うので、

go mod tidy

で assert の依存関係を追加します。

 

テストコードの大まかな実装の参考は gomockリファレンス より。

先に生成した mock を、mock_db としてインポートしています。

mockObj.EXPECT().{テスト対象関数}.RETURN({戻り値})

上記の形式で戻り値を指定できます。

 

その後、上記で設定した mock を、Service のパラメーターとして下記のように渡し、Service のメソッドを呼び出してテストを実行します。

service := BookService{Repository: mockObj}

 

go test
PASS
ok      unittest/service        0.004s

 

テストがPASSしました。

 

ちゃんと設定した内容が mock に反映されているか確認するため、わざと失敗のテストケースにしてみます。

 

func TestGetBookById(t *testing.T) {
    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()

    expectedBook := model.Book{Id: 100, Name: "テストブック", Price: 1500}

    mockObj := mock_db.NewMockBookRepository(mockCtrl)
    mockObj.EXPECT().GetBookById(100).Return(expectedBook)

    service := BookService{Repository: mockObj}

    actualBook := service.GetBookById(200)

    assert.Equal(t, expectedBook, actualBook)
}

 

go test
--- FAIL: TestGetBookById (0.00s)
    book_service.go:13: Unexpected call to *mock_repository.MockBookRepository.GetBookById([200]) at /root/repos/go-samples/unittest/service/book_service.go:13 because: 
        expected call at /root/repos/go-samples/unittest/service/book_service_test.go:19 doesn't match the argument at index 0.
        Got: 200 (int)
        Want: is equal to 100 (int)
    controller.go:269: missing call(s) to *mock_repository.MockBookRepository.GetBookById(is equal to 100 (int)) /root/repos/go-samples/unittest/service/book_service_test.go:19
    controller.go:269: aborting test due to missing call(s)
FAIL
exit status 1
FAIL    unittest/service        0.005s

 

テストが失敗することが確認できました。

 

以上サンプルになります。

 

参考

qiita.com