Help us understand the problem. What is going on with this article?

Go言語でユニットテスト, テストしやすいコードとモックを書く

Go言語でテストしやすいコードを書く

Go言語でテスタブル(テストしやすい)コードを書いてユニットテストを実践しましょう.
この記事では以下について書きます.ユニットテストの概要については書きません.

  • Go言語でのユニットテストの書き方
  • テストアサーション
  • Go言語でのユニットテストの実行方法
  • テストしやすいコードの書き方
  • Go言語でのモックの作成とモックの使い方

Go言語でのユニットテストの書き方

テストコードのファイル(xxxx_test.go)

Go言語では本体コードと同じパッケージ内(同じディレクトリ内)にテストファイルを作成するのが一般的のようです. ファイル名はxxx_test.goにするのがルールです.

├── calc.go
└── calc_test.go

テストコードの基本

テストコードでは以下のパッケージを使います.

関数はTestXXXXという名称で引数に*testing.T型を取る必要があります.
関数名は日本語も含めることができます.テストコードではテスト項目をよりわかりやすくするために開発チームが最も理解できる言葉で書く場合があります(筆者個人としては,テストコードに限っては日本語関数名を勧めて言います.).

func TestGetUser_正常系(t *testing.T) {
}

func TestGetUser_データベース接続障害時の場合にエラーを返すことを検証(t *testing.T) {
}

例としてsumという関数のテストを書いてみます.

calc.go
func sum(a, b int) int {
    return a + b
}
calc_test.go
import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestSum(t *testing.T) {
    actual := sum(3, 5)
    expected := 8

    assert.Equal(t, expected, actual)
}`

アサーション

if文で値をチェックしてt.Fail()することでもテストできますが、"github.com/stretchr/testify/assert"の関数を使うことで読みやすいコードを書くことができます。
詳細についてはassertのgodocを参照してください。

以下よく使うであろうアサーションを紹介します。

// 値が等しいかどうかを確認
assert.Equal(t, expected, actual)

// 値がNilであるか確認
assert.Nil(t, actual)

// 値がNilでないか確認
assert.NotNil(t, actual)

// エラー発生であることを確認
assert.Error(t, err)

// エラー発生でないことを確認
assert.NoError(t, err)

ユニットテストの実行

goコマンドでユニットテストを実行できます.

$ go test calc_test.go

もしくは次のようにすることでカレントディレクトリ配下のすべてのテストを実行できます.

$ go test ./....

また,-vオプションを付けることでテストの実行状態の詳細を出力できます.

go test -v  calc_test.go
go test -v ./...

ここまでGo言語におけるユニットテストの基本でした.

テストしやすいコードの書き方

テストしやすいコードとは?

ユニットテストを積極的に導入するには予め本体コードをテストしやすいコードにしておく必要があります.そのためには以下の心がけが必要です.この心がけは言語を問わず言えることです.Go言語に限ったことではありません.

  • 関数に外部依存の要素を作らない.
    • 外部依存の要素は引数で注入できるようにする.
    • 外部依存を含む関数はまとめる.
  • 一つの関数でたくさんの処理をしない.
    • これは保守性の高いコードを書く上で基本.
  • モック化できるようにコードを書く

モック化できるコード

本記事では,Go言語において「モック化できるように書く」ことを記述します.
ユニットテスト時においてモック化とはテストに依存する外部依存の関数の動作をテストのためにふるまいを置き換えることを言います.

例えば,テスト対象の関数内でデータベースに接続してユーザ情報を取得する処理を行っている場合,モック化することでユニットテストでは実際にはデータベースに接続しませんが,接続した体で値を返してテストを進めると言ったことです.

Go言語においてモック化できるコードにするためには以下のようにします.

  • 構造体に関数を実装する
  • その構造体のインターフェイスを外部公開する

構造体の置き換えはできませんが,インターフェイスであれば同じ入出力の関数を持てば置き換えができます.

具体的には以下のようにします.

user_db.go
type UserDB interface {
    GetUser(userID string) (*model.User, error)
}

func NewUserDB() UserDB {
    return &userDB{}
}

type userDB struct {
}

func (db *userDB) GetUser(userID string) (*model.User, error) {
}

この例は,ユーザデータベースからユーザ情報を取得する関数を書いています.
userDBは実際に処理を行う関数を持つ構造体ですが,小文字から始まっているように外部公開していません.一方で,この構造体のインターフェイスであるUserDBは外部公開しています.また,userDB生成する関数NewUserDBは外部公開しています.そしてこの関数の戻り値は構造体userDBではなくインターフェイスUserDBを指しています,

従って,呼び出し側は以下のようになります.

db := NewUserDB()
user, err := db.GetUser("12")

このときUserDBを外部注入できるようにしておけばテストしやすいコードを書くことができます.
具体的には以下のようにします. この例ではインターフェイスUserDBに依存するUserServiceを記述しています.ポイントは構造体ではなくインターフェイスにしている点です.これのよりUserDBを置き換えるとができます.

struct userService{
    db: UserDB
}

func (s *userService) GetUser(userID string){
   user, err := s.db.GetUser(userID)
   if err != nil {
       //....エラー時の処理       
   }

   //...正常系処理
}

func main() {
    service := userService{ db: NewUserDB() }
    service.GetUser("hoge")
}

モックの作成

Go言語ではインターフェイスからモックを自動生成することができます.モックを作って,本来の処理を置換してテストを書きましょう.

モックの生成コマンド

モックの生成にはmockgenコマンドを使用します.
例えば,以下のようにすることで,user_db.goのモックがmocks/配下に作成されます.最後のpackageオプションで生成するモックが属するパッケージを指定しています.

go get github.com/golang/mock/mockgen
mockgen -soruce user_db.go -destination mocks/user_db.go -package mocks

モックを使ったテストコード

生成したモックは元とインターフェイスを持つので置き換えることができます.そしてモック特有の関数で振る舞いを変えることができます.

func TestGetUser_正常系(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish(()

    dbMock := mocks.NewMockUserDB(ctrl)
    dbMock.EXPECT().GetUser("user1").Return(&model.User{}, nil)

    service := userService{ db: dbMock }
    userService.GetUser("user1")
}

これは,生成したモックを使ったユニットテストの例です.
ctrlの生成についてはいわゆるおまじないと言っていいでしょう.

mocks.NewMockUserDBにてモックを生成しています.次行のEXPECT()...の部分で,期待される呼び出し方とふるまいについて定義しています.ここで期待したとおりに関数が呼び出されなければテストエラーとなります.

モックの使い方

モックの詳細な使い方については公式リファレンスを参照してください.
ここでは,よく使うであろう使い方について記述します.

引数チェック

EXPECT().関数名(期待したい値)を記述します・「期待したい値」で呼び出しがなければテストエラーとなります.

dbMock.EXPECT().GetUser("user-id1")

何でもいい引数

引数ななんでも良い場合は,gomock.Any()を指定します.

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

dbMock.EXPECT().GetUser(gomock.Any())

戻り値の振る舞い

戻り値を指定したい場合はReturnで指定します.

expectedUser := model.User{}

dbMock.EXPECT().GetUser("user-id1").Return(expectedUser, nil)

代入の振る舞い

戻り値ではなく,ポインタ引数に代入したい場合はSetArgを指定します.

product_db.go
func (db *productDB) GetProduct(data *model.Product) {
    data = &model.Product{}
}

この場合,以下のようにすることで引数に代入した振る舞いができます.

expectedProduct := &model.Product{}
dbMock.EXPECT().GetProduct.SetArg(0, expectedProduct)

まとめ

Go言語におけるユニットテストとユニットテストを書きやすくするための実装方法,モック化について書きました.

Tips

モックの生成時にエラーになったら..

モック生成後,元の構造体・インターフェイスを変更後に再度生成する際にエラーになる場合があります.
大抵は既存のコードと新しいコードで関係性が維持できない場合.

そんなときは,問題発生原因のコードをビルド対象から外すとモックが生成できるようになるかもしれません.以下の記述をソースコードの1行目に加えると外れます.

// +build test
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away