4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

gomockでmockを作ってみる[Go]

Last updated at Posted at 2024-09-30

gomockとは?

gomockとは、インターフェース定義からモックの生成を行うことができるgoのライブラリで、go公式から出されています。

しかし、READMEには

Update, June 2023: This repo and tool are no longer maintained. Please see go.uber.org/mock for a maintained fork instead.

と書かれており2023でメンテナンスが終了しているようでした。

READMEにも書かれているようにgo.uber.org/mockがその代わりとして使用されています。

go.uber.org/mockは、かの有名なUberEatsを運営しているUberがgomockのメンテナンスを引き継いだライブラリです。

今回は、go.uber.org/mockでモックを作成してみようと思います。

そもそもモックって?

個人開発のみをしている学生やプログラミングやりたての人からすると、あまり聞き慣れた単語ではないと思います。

モックは、その名の通り模型のことで、アプリケーションのテストをするために必要な部品を作成することを言います。

使用ケースとしては、データベースなどの外部に影響を受ける部分をテストする場合に、テストしたい部分がデータベースからデータを取り出すことでないことがあります。その場合は、取り出せたと仮定して動いてくれた方がテストで調べたい内容を正確に調べることができるため使用されます。

こう説明してみても、抽象的でわかりずらいと思うので、実際に実装してみようと思います。

実装してみる

userの登録機能を例に、ユースケース層でのテストを実装してみようと思います。

例えば、下のような感じの構造で実装したとします。

.
├── domain
│   ├── entity
│   ├── mockreposiotry
│   ├── mockrepository
│   └── repository
├── infrastructure
├── main.go
├── repository
└── service(ユースケース)

ただ、今回のようにユースケースでテストを行うだけなら下のような感じでも動きます。

.
├── domain
│   ├── entity
│   ├── mockreposiotry
│   ├── mockrepository
│   └── repository
├── main.go
├── repository
└── service(ユースケース)

下準備

今回は、ユースケースでのテスト実装に焦点を当てるので、最小限の実装コードのみを載せます。

  • Userモデル定義
    dbから取り出して扱うGoの構造体を定義します。
domain/enitity/user.go
package entity

type User struct {
	ID   int
	Name string
}
  • repositoryのインターフェース
    dbから取り出す操作をインターフェースとして抽象定義しています。
domain/repository/user.go
package repository

import "example.com/user/domain/entity"

type IUserRepository interface {
	CreateUser(name string) (*entity.User, error)
}

  • repositoryの実装
    先ほど定義したインターフェースの中身を実装しています。
repository/user.go
package repository

import (
	"example.com/user/domain/entity"
	"gorm.io/gorm"
)

type UserRepository struct {
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
	return &UserRepository{db: db}
}

func (r *UserRepository) CreateUser(name string) (*entity.User, error) {

	user := &entity.User{Name: name}

	r.db.Create(user)

	return user, nil
}

Goのインターフェース継承は、ダックタイピングで行われます。
ダックタイピングは、インターフェースに定義したメソッドを実装した構造体があれば、自動的にインターフェースが継承されたとみなされます。

  • serviceの実装
    ユースケースを実装しています。
    ここでは、repositoryインターフェースからデータを取り出すことを行っています。
service/user.go
package service

import (
	"example.com/user/domain/entity"
	"example.com/user/domain/repository"
)

type UserService struct {
	repo repository.IUserRepository
}

func NewUserService(repo repository.IUserRepository) *UserService {
	return &UserService{repo: repo}
}

func (s *UserService) CreateUser(name string) (*entity.User, error) {
	user, err := s.repo.CreateUser(name)
	if err != nil {
		return nil, err
	}
	return user, nil
}

モックの作成

ここから実際にモックを作成してみます。

インストール

go.uber.org/mockのインストールは下のようにします。

go get go.uber.org/mock/mockgen
go install go.uber.org/mock/mockgen@latest

go.uber.org/mockGithubリポジトリのREADMEgo getの文面見つからないかもしれませんがつけてください。

go installだけだと依存関係を管理するgo modには追加されません。
go getも行ってください

インストールできたら下のコマンドで入っているかを確認します。

mockgen -version

もし入っていなかった場合は、GOPATHを通してください

export PATH=$PATH:$(go env GOPATH)/bin

モックを生成する

下のコマンドでモックを作成します。

mockgen -source=domain/repository/user.go -destination=domain/mockreposiotry/user.go -package mockrepository

-sourceでモックにしたいインターフェースが定義されているファイルを指定します。
-destinationでモックを生成する場所を指定します。
packageでモックのパッケージ名を指定します。

生成されたコードは下のようになります。

// Code generated by MockGen. DO NOT EDIT.
// Source: domain/repository/user.go
//
// Generated by this command:
//
//	mockgen -source=domain/repository/user.go -destination=domain/mockreposiotry/user.go -package mockrepository
//

// Package mockrepository is a generated GoMock package.
package mockrepository

import (
	reflect "reflect"

	entity "example.com/user/domain/entity"
	gomock "go.uber.org/mock/gomock"
)

// MockIUserRepository is a mock of IUserRepository interface.
type MockIUserRepository struct {
	ctrl     *gomock.Controller
	recorder *MockIUserRepositoryMockRecorder
}

// MockIUserRepositoryMockRecorder is the mock recorder for MockIUserRepository.
type MockIUserRepositoryMockRecorder struct {
	mock *MockIUserRepository
}

// NewMockIUserRepository creates a new mock instance.
func NewMockIUserRepository(ctrl *gomock.Controller) *MockIUserRepository {
	mock := &MockIUserRepository{ctrl: ctrl}
	mock.recorder = &MockIUserRepositoryMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIUserRepository) EXPECT() *MockIUserRepositoryMockRecorder {
	return m.recorder
}

// CreateUser mocks base method.
func (m *MockIUserRepository) CreateUser(name string) (*entity.User, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "CreateUser", name)
	ret0, _ := ret[0].(*entity.User)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// CreateUser indicates an expected call of CreateUser.
func (mr *MockIUserRepositoryMockRecorder) CreateUser(name any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockIUserRepository)(nil).CreateUser), name)
}

コメントアウトにも書かれていますが、このファイルは編集するのはダメです。

モックを使ってみる

実際に生成したモックでテストを書いてみます。

package service

import (
	"testing"

	"example.com/user/domain/entity"
	mockrepository "example.com/user/domain/mockreposiotry"
	gomock "go.uber.org/mock/gomock"
)

func TestCreateUser(t *testing.T) {
	ctrl := gomock.NewController(t)

	mr := mockrepository.NewMockIUserRepository(ctrl)

	svc := NewUserService(mr)

	mr.EXPECT().CreateUser("John").Return(&entity.User{ID: 1, Name: "John"}, nil).Times(1)

	user, err := svc.CreateUser("John")
	if err != nil {
		t.Errorf("Error was not expected, got: %v", err)
	}

	if user.Name != "John" {
		t.Errorf("User name is not correct, got: %s", user.Name)
	}
	if user.ID != 1 {
		t.Errorf("User ID is not correct, got: %v", user.ID)
	}

}

実行してみると下のようになります。

go test -v
=== RUN   TestCreateUser
--- PASS: TestCreateUser (0.00s)
PASS
ok  	example.com/user/service	0.411s

テストコードの説明

まず、モックの呼び出しに必要なコントローラーを生成します。

ctrl := gomock.NewController(t)

生成したコントローラーを使ってモックリポジトリを生成させます。

mr := mockrepository.NewMockIUserRepository(ctrl)

作成したモックを依存関係として、NewUserService()にDIすることで、serviceオブジェクトを生成します。

svc := NewUserService(mr)

DI(Dependacy Injection)とは

依存性注入と呼ばれる方法で、インターフェースを依存関係として入れることで、複雑なオブジェクトを依存させない方法です。

オブジェクト指向プログラミングでは、コンストラクタにインスタンスの生成に必要な依存関係を記述します。
Goではコンストラクタにインターフェースが依存関係にあることを明記することで、実際にインスタンスを生成するときにインターフェースを継承したものであればどんなものでもインスタンスメソッドの引数に入れることができます。

今回でいうとNewUserService()の引数は、インターフェースを継承したMockIUserRepositoryNewUserRepositoryも入れてインスタンスを生成することが可能です。

モックリポジトリにデータを入れます。

mr.EXPECT().CreateUser("John").Return(&entity.User{ID: 1, Name: "John"}, nil).Times(1)

EXPECT()は、mockgenで生成させたmockrepository/user.goの中で下のように定義されています。
ここでは、recorder(今回だとMockIUserRepositoryMockRecorder)を返しています

func (m *MockIUserRepository) EXPECT() *MockIUserRepositoryMockRecorder {
	return m.recorder
}

CreateUser()は、実は生成されたモックに2つ定義されています。

func (m *MockIUserRepository) CreateUser(name string) (*entity.User, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "CreateUser", name)
	ret0, _ := ret[0].(*entity.User)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

func (mr *MockIUserRepositoryMockRecorder) CreateUser(name any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockIUserRepository)(nil).CreateUser), name)
}

直前のEXPECT()でrecorderを返しているので、今回は下のCreateUser()が使用されています。

Return()では、モックが結果的にどのようなデータを返すかを定義しています。
今回は、モックリポジトリがdbにIDが1の名前が"John"という人のデータを入れたという仮の結果を想定しています。

Times(1)では、使用するサービスのメソッド内で何回CreateUser()が呼び出されるかを制御しています。

再掲しますが、svc.CreateUser()は下のような実装になっていて、リポジトリのCreateUser()は1回しか呼び出されていません。なので、Times(1)と設定しています。

func (s *UserService) CreateUser(name string) (*entity.User, error) {
	user, err := s.repo.CreateUser(name)
	if err != nil {
		return nil, err
	}
	return user, nil
}

Times()について

Times(1)と書いていますが、実はデフォルトでTimes(1)となっているため、書かなくてもいいです。
しかし、Times()の引数の数字はもし1回以上呼ばれる場合、Times()もしくはAnyTimes(実行回数制限かけないメソッド)を使わないとエラーを吐いてしまうので、気をつけてください。

Times()がデフォルトで1となっているのは、MinTimes()に下のような記述があります。
MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called or if MaxTimes was previously called with 1, MinTimes also sets the maximum number of calls to infinity.

4
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?