プログラミング

GO言語の基本-テストコード 初心者のためのgomockの使い方

GO言語の基本-テストコード 初心者のためのgomockの使い方

概要

フリーランスエンジニア スリーネクスト

GO言語のテストコードモジュール GoMockを紹介してます。

GoMockはテストを実行するときにDBや外部APIを疎通しなくてもプログラミングが正しく動いているかどうかテストコードで確認することができます。

GoMockの導入手順

GoMockを使ったコードはGithub上で公開しています。HomeAPIというプロジェクトの中でGoMockテストコードを記載しています。以下のGitHubボタンをクリックするとGitHubページに遷移します

GitHubボタン

GoMock インストール

go mockを利用するにはモジュールをインストールする必要があります。
以下のコマンドを入力してください。

go get -u github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen

Go 1.16以上 go instalコマンドが1.16以上のため

GoMock コードを導入

go generateをコード内に記入

Go言語ではコードを自動生成するコマンド go generate が存在します。

クリーンアーキテクチャの設計であればrepository インターフェース層の以下にgo:generateを追記します。

//go:generate mockgen -source=./temperature_repository.go -package=repositorymock -destination=./mock/temperature_repository.go

全体的なコード

package repository

import (
	"homeapi/domain"
	"homeapi/interfaces/repository"

	"github.com/jinzhu/gorm"
)

//go:generate mockgen -source=./temperature_repository.go -package=repositorymock -destination=./mock/temperature_repository.go

func NewTemperatureRepository(db *gorm.DB) repository.TemperatureRepository {
	return repository.TemperatureRepository{
		Database: db,
	}
}

// TemperatureRepository Temperature Repository
type TemperatureRepository interface {
	List() ([]domain.Temperature, error)
	Insert(*domain.Temperature) error
}

プロジェクト直下でコマンド

go generateのコメントを入れたら以下のコマンドを実行してください。

go generate ./...

自動生成されたコード

インターフェースに合わせたリクエストとレスポンスの値に対してモックが作成されます。

// Code generated by MockGen. DO NOT EDIT.
// Source: ./temperature_repository.go

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

import (
	domain "homeapi/domain"
	reflect "reflect"

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

// MockTemperatureRepository is a mock of TemperatureRepository interface.
type MockTemperatureRepository struct {
	ctrl     *gomock.Controller
	recorder *MockTemperatureRepositoryMockRecorder
}

// MockTemperatureRepositoryMockRecorder is the mock recorder for MockTemperatureRepository.
type MockTemperatureRepositoryMockRecorder struct {
	mock *MockTemperatureRepository
}

// NewMockTemperatureRepository creates a new mock instance.
func NewMockTemperatureRepository(ctrl *gomock.Controller) *MockTemperatureRepository {
	mock := &MockTemperatureRepository{ctrl: ctrl}
	mock.recorder = &MockTemperatureRepositoryMockRecorder{mock}
	return mock
}

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

// Insert mocks base method.
func (m *MockTemperatureRepository) Insert(arg0 *domain.Temperature) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Insert", arg0)
	ret0, _ := ret[0].(error)
	return ret0
}

// Insert indicates an expected call of Insert.
func (mr *MockTemperatureRepositoryMockRecorder) Insert(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockTemperatureRepository)(nil).Insert), arg0)
}

// List mocks base method.
func (m *MockTemperatureRepository) List() ([]domain.Temperature, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "List")
	ret0, _ := ret[0].([]domain.Temperature)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// List indicates an expected call of List.
func (mr *MockTemperatureRepositoryMockRecorder) List() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTemperatureRepository)(nil).List))
}

このコードを修正するのはNGです。同じコマンドを打つとまた元に戻ってしまいます。

テストコード実装

テストコードを書いている場所はこちらをクリック

取得系のコード(SELECT)

type serverMocks struct {
	temperatureRepository *repositorymock.MockTemperatureRepository
}

func newMocks(ctrl *gomock.Controller) (*TemperatureUsecase, *serverMocks) {
	mocks := &serverMocks{
		temperatureRepository: repositorymock.NewMockTemperatureRepository(ctrl),
	}

	temperatureUsecase := &TemperatureUsecase{
		TemperatureRepository: mocks.temperatureRepository,
	}

	return temperatureUsecase, mocks
}

func TestList(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		nowTime := time.Now()
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		temperatureUsecase, mocks := newMocks(ctrl)
		temperature := []domain.Temperature{
			{
				ID:        12,
				Temp:      "22",
				Humi:      "61",
				CreatedAt: nowTime,
			},
			{
				ID:        13,
				Temp:      "25",
				Humi:      "63",
				CreatedAt: nowTime,
			},
		}

		mocks.temperatureRepository.EXPECT().List().Return(temperature, nil)
		got, err := temperatureUsecase.List()
		require.NoError(t, err)
		if err != nil {
			t.Errorf("error message : %v", err)
		}
		want := []ports.TemperatureOutputPort{
			{
				ID:   12,
				Temp: "22",
				Humi: "61",
			},
			{
				ID:   13,
				Temp: "25",
				Humi: "63",
			},
		}
		assert.Equal(t, &want, got)
	})
}

挿入系のコード(INSERT)

func TestInsert(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		nowTime, err := util.JapaneseNowTime()
		if err != nil {
			t.Error(err)
		}
		// nowTime := time.Now()
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		temperatureUsecase, mocks := newMocks(ctrl)

		temperature := &domain.Temperature{
			Temp:      "20",
			Humi:      "55",
			CreatedAt: nowTime,
		}

		dofunc := func(temperature *domain.Temperature) *domain.Temperature {
			temperature.ID = 71
			return temperature
		}

		mocks.temperatureRepository.EXPECT().Insert(temperature).Do(dofunc).Return(nil)

		request := &ports.TemperatureInputPort{
			Temp: "20",
			Humi: "55",
		}

		got, err := temperatureUsecase.Create(request)
		require.NoError(t, err)

		want := ports.TemperatureOutputPort{
			ID:   71,
			Temp: "20",
			Humi: "55",
		}
		assert.Equal(t, want, got)
	})
}

ハマりポイント

Listに対するコード
mocks.temperatureRepository.EXPECT().List().Return(temperature, nil)

Insertに対するコード
mocks.temperatureRepository.EXPECT().Insert(temperature).Do(dofunc).Return(nil)

EXPECT()の部分はなかなかテストが通りずらいことを経験された方が多いです。具体的になにを入れれがいいのか解説していきます。データは正しく挿入しないと動かないということとエラーメッセージがわかりずらいところです。

インターフェースが以下のコードに対するテストコードです。
Listに対するインターフェース
List() ([]domain.Temperature, error)

Insertに対するインターフェース
Insert(*domain.Temperature) error

基本的に同じ型のデータを入れてあげればいいのですがInsertにあるdofuncはID等のデータベースで自動で割り振られるものを定義しておくことで予想するテスト結果を返すことができます。

まとめ

テストではDBぐらいはテストDB立ち上げてテストした方がより明確にテストできるのでいいですが、外部APIを利用する時はまさにMockを使った方が安心して利用することができます。

-プログラミング