6
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?

More than 5 years have passed since last update.

Go で DIP に従って依存関係を逆転させたいときは ifacemaker と moq の組み合わせが強い

Posted at

はじめに

  • Go で DIP(依存関係逆転の法則)に従って依存関係を逆転させたくなったとき ifacemaker を使って構造体からインターフェイスを生成して moq を使ってインターフェイスからモックを作るのが便利

  • 構造体 User の値をメモリに登録・参照するリポジトリ UserMemoryRepository がある
  • UserMemoryRepository をサービスなどから利用したくなったとして、直接参照するのではなく UserMemoryRepository を抽象化したインターフェイスを参照するようにしたいとする
Userの定義
type User struct {
	ID   int
	Name string
}
UserMemoryRepositoryの定義
type UserMemoryRepository struct {
}

var userMemoryRepositoryMap = map[int]*User{}

func (r *UserMemoryRepository) Get(id int) (*User, bool) {
	u, ok := userMemoryRepositoryMap[id]
	return u, ok
}

func (r *UserMemoryRepository) Put(u *User) error {
	userMemoryRepositoryMap[u.ID] = u
	return nil
}

構造体からインターフェイスを生成する

  • UserMemoryRepository を抽象化して扱うために ifacemaker を使ってインターフェイスを生成する
実行コマンド
ifacemaker -f user.go -s UserMemoryRepository -i UserRepository -p user -o user_interface.go
生成されたインターフェイスUserRepository
package user

// UserRepository ...
type UserRepository interface {
	Get(id int) (*User, bool)
	Put(u *User) error
}

インターフェイスからモックを生成する

  • UserMemoryRepository のインターフェイスとして生成した UserRepository を使ったテストをしやすいように moq を使ってモックを生成する
実行コマンド
moq . UserRepository -out user_mock.go
生成されたモック
生成されたモック
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq

package user

import (
	"sync"
)

var (
	lockUserRepositoryMockGet sync.RWMutex
	lockUserRepositoryMockPut sync.RWMutex
)

// Ensure, that UserRepositoryMock does implement UserRepository.
// If this is not the case, regenerate this file with moq.
var _ UserRepository = &UserRepositoryMock{}

// UserRepositoryMock is a mock implementation of UserRepository.
//
//     func TestSomethingThatUsesUserRepository(t *testing.T) {
//
//         // make and configure a mocked UserRepository
//         mockedUserRepository := &UserRepositoryMock{
//             GetFunc: func(id int) (*User, bool) {
// 	               panic("mock out the Get method")
//             },
//             PutFunc: func(u *User) error {
// 	               panic("mock out the Put method")
//             },
//         }
//
//         // use mockedUserRepository in code that requires UserRepository
//         // and then make assertions.
//
//     }
type UserRepositoryMock struct {
	// GetFunc mocks the Get method.
	GetFunc func(id int) (*User, bool)

	// PutFunc mocks the Put method.
	PutFunc func(u *User) error

	// calls tracks calls to the methods.
	calls struct {
		// Get holds details about calls to the Get method.
		Get []struct {
			// ID is the id argument value.
			ID int
		}
		// Put holds details about calls to the Put method.
		Put []struct {
			// U is the u argument value.
			U *User
		}
	}
}

// Get calls GetFunc.
func (mock *UserRepositoryMock) Get(id int) (*User, bool) {
	if mock.GetFunc == nil {
		panic("UserRepositoryMock.GetFunc: method is nil but UserRepository.Get was just called")
	}
	callInfo := struct {
		ID int
	}{
		ID: id,
	}
	lockUserRepositoryMockGet.Lock()
	mock.calls.Get = append(mock.calls.Get, callInfo)
	lockUserRepositoryMockGet.Unlock()
	return mock.GetFunc(id)
}

// GetCalls gets all the calls that were made to Get.
// Check the length with:
//     len(mockedUserRepository.GetCalls())
func (mock *UserRepositoryMock) GetCalls() []struct {
	ID int
} {
	var calls []struct {
		ID int
	}
	lockUserRepositoryMockGet.RLock()
	calls = mock.calls.Get
	lockUserRepositoryMockGet.RUnlock()
	return calls
}

// Put calls PutFunc.
func (mock *UserRepositoryMock) Put(u *User) error {
	if mock.PutFunc == nil {
		panic("UserRepositoryMock.PutFunc: method is nil but UserRepository.Put was just called")
	}
	callInfo := struct {
		U *User
	}{
		U: u,
	}
	lockUserRepositoryMockPut.Lock()
	mock.calls.Put = append(mock.calls.Put, callInfo)
	lockUserRepositoryMockPut.Unlock()
	return mock.PutFunc(u)
}

// PutCalls gets all the calls that were made to Put.
// Check the length with:
//     len(mockedUserRepository.PutCalls())
func (mock *UserRepositoryMock) PutCalls() []struct {
	U *User
} {
	var calls []struct {
		U *User
	}
	lockUserRepositoryMockPut.RLock()
	calls = mock.calls.Put
	lockUserRepositoryMockPut.RUnlock()
	return calls
}

これまでのコード生成を go generate で実行する

  • 構造体からインターフェイスの生成、インターフェイスからモックの生成、それぞれを go generate コマンドひとつでまとめて実行できるように go:generate コメントを追記してく
UserMemoryRepositoryの定義の上にコメントを追記
//go:generate ifacemaker -f user.go -s UserMemoryRepository -i UserRepository -p user -o user_interface.go
//go:generate moq -out user_mock.go . UserRepository
type UserMemoryRepository struct {

モックを使ってテストする

  • 生成したモックを使ってテストを書いてみる
  • moq で生成したモック構造体を使う場合は初期化時に必要な関数の実装を関数リテラルで指定するだけなので直感的
package user

import "testing"

func TestUserRepository_Get(t *testing.T) {
	r := UserRepositoryMock{
		GetFunc: func(id int) (*User, bool) {
			return &User{}, true
		},
	}
	if _, ok := r.Get(100); !ok {
		t.Fatal("Get must ok.")
	}
}

おわりに

  • testing はテスト用のミニ言語とか読むのも覚えるのもだるいからやめようぜっていう方向性なのに gomock が公式みたいな顔して独自の記法をゴリ押ししてくるのが納得いってないので moq を使ってやればいいとおもいました。
6
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
6
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?