gomock を使ってテストを書く
gomock を使ってモックを生成してテストを書く。
FYI:go-sqlmock で SQL 実行をモックするのは go-sqlmock でテストをとりあえず書く
開発環境
- go version go1.18.1 linux/amd64
本文
Go でテストを書く場合、モックを gomock を利用して自動生成できるので、それを使ってテストを書く。
モック生成
まずモック生成ツールをインストールする。
go install github.com/golang/mock/[email protected]
下記のようにコマンドを打ってモックを生成する。
mockgen -source=./internal/repository/article/article.go -destination=./internal/mock/mock_article/mock_article.go
source がモックを作りたいソースコードのパス、destination がモックのファイル生成先である。
モックのパッケージ名は「mock_[元のパッケージ名]」になるようなので、生成先のディレクトリ・ファイル名はそれを見越しておくと良いと思う。
また注意点として private メソッドはモックが生成されない。
テストを書く
テストしたいソースが下記であるとする。
package mparameter
import (
"gitlab.com/k1350/sololog_gql/graph/model"
"gitlab.com/k1350/sololog_gql/internal/repository/mparameter"
)
type IMParameterUsecase interface {
Get() (*model.MasterParameter, error)
}
type MParameterUsecase struct {
mParameterRepository mparameter.IMParameterRepository
}
func NewMParameterUsecase(r mparameter.IMParameterRepository) IMParameterUsecase {
return &MParameterUsecase{mParameterRepository: r}
}
func (u *MParameterUsecase) Get() (*model.MasterParameter, error) {
return u.mParameterRepository.Find()
}
これをテストするために u.mParameterRepository.Find()
を gomock でモック化する。
これがモック対象のソースコードで、これが自動生成したモックである。
テスト例は下記のようになる。
t.Run("正常系", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_mparameter.NewMockIMParameterRepository(ctrl)
u := NewMParameterUsecase(m)
expected := &model.MasterParameter{
MaxBlogs: 5,
MaxBlogLinks: 3,
}
m.
EXPECT().
Find().
Return(expected, nil)
result, err := u.Get()
if err != nil {
t.Error("got err:", err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("got:\n%v\n\nwant:\n%v", result, expected)
}
})
まず
ctrl := gomock.NewController(t)
defer ctrl.Finish()
でコントローラを生成する。
次に
m := mock_mparameter.NewMockIMParameterRepository(ctrl)
でモック対象を生成する。
引数や戻り値は
m.
EXPECT().
Find().
Return(expected, nil)
の部分で指定する(この例は引数が無い)。
異常系も含めたテスト例はこんな感じ。
EXPECT の記述が不足している、または EXPECT が書いてあるのに呼ばれなかった場合はテスト実行時にエラーとなる。
sql-mock と併用する
下記のようにリポジトリの外でトランザクションを張っている場合
func (u *BlogUsecase) DeleteBlog(now time.Time, blogId string, userId int) error {
intId, _ := strconv.ParseInt(blogId, 10, 64)
// 最初に削除対象の存在チェック&権限チェックする
blog, err := u.blogRepository.FindById(intId, userId)
if err != nil {
return err
}
if blog == nil {
return customError.NotFoundError
}
// トランザクション開始
tx, err := u.blogRepository.BeginTransaction()
if err != nil {
return err
}
defer func() {
// panicが起きたらロールバック
if p := recover(); p != nil {
tx.Rollback()
panic(p)
}
}()
err = u.blogRepository.DeleteById(tx, now, intId, userId)
if err != nil {
tx.Rollback()
return err
}
err = u.articleRepository.DeleteByBlogId(tx, now, intId)
if err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}
テストの際、sql-mock と gomock によるモックを併用する。
以下に書いたテストコードを抜粋する。
func TestDeleteBlog(t *testing.T) {
now := time.Now().UTC()
var blogId int64 = 2
stringBlogId := "2"
blog := &model.Blog{
ID: "1",
BlogKey: "dccb4797-90c7-ea22-6866-7eb954acbbda",
Author: "著者1",
Name: "ブログ名1",
Description: "",
PublishOption: model.PublishOptionPublic,
CreatedAt: now.Format(time.RFC3339),
UpdatedAt: now.Format(time.RFC3339),
}
tests := []struct {
name string
blogFindExpected *model.Blog
blogFindErr error
deleteBlogErr error
deleteArticleErr error
wantErr bool
}{
{
name: "正常系",
blogFindExpected: blog,
blogFindErr: nil,
deleteBlogErr: nil,
deleteArticleErr: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db, dbmock, err := sqlmock.New()
if err != nil {
fmt.Println("failed to open sqlmock database:", err)
}
defer db.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mb := mock_blog.NewMockIBlogRepository(ctrl)
ma := mock_article.NewMockIArticleRepository(ctrl)
u := NewBlogUsecase(mb, ma)
mb.
EXPECT().
FindById(blogId, 1).
Return(tt.blogFindExpected, tt.blogFindErr)
if tt.blogFindExpected != nil && tt.blogFindErr == nil {
dbmock.ExpectBegin()
tx, err := db.Begin()
if err != nil {
fmt.Println("failed to begin transaction:", err)
}
mb.
EXPECT().
BeginTransaction().
Return(tx, nil)
mb.
EXPECT().
DeleteById(tx, now, blogId, 1).
Return(tt.deleteBlogErr)
if tt.deleteBlogErr == nil {
ma.
EXPECT().
DeleteByBlogId(tx, now, blogId).
Return(tt.deleteArticleErr)
dbmock.ExpectCommit()
}
}
err = u.DeleteBlog(now, stringBlogId, 1)
if tt.blogFindErr == nil && err != nil {
dbmock.ExpectRollback()
}
if (err != nil) != tt.wantErr {
t.Error("got err:", err)
}
})
}
}
ソースコード全体はここ。
以上