Go言語でテストしやすいコードを書く
Go言語でテスタブル(テストしやすい)コードを書いてユニットテストを実践しましょう.
この記事では以下について書きます.ユニットテストの概要については書きません.
- Go言語でのユニットテストの書き方
- テストアサーション
- Go言語でのユニットテストの実行方法
- テストしやすいコードの書き方
- Go言語でのモックの作成とモックの使い方
Go言語でのユニットテストの書き方
テストコードのファイル(xxxx_test.go)
Go言語では本体コードと同じパッケージ内(同じディレクトリ内)にテストファイルを作成するのが一般的のようです. ファイル名はxxx_test.go
にするのがルールです.
├── calc.go
└── calc_test.go
テストコードの基本
テストコードでは以下のパッケージを使います.
- testing (必須)
- github.com/stretchr/testify/assert (アサーション用, 必須ではないがあると便利)
関数はTestXXXX
という名称で引数に*testing.T
型を取る必要があります.
関数名は日本語も含めることができます.テストコードではテスト項目をよりわかりやすくするために開発チームが最も理解できる言葉で書く場合があります(筆者個人としては,チーム全員が日本人であれば、テストコードに限っては日本語関数名を勧めています.).
func TestGetUser_正常系(t *testing.T) {
}
func TestGetUser_データベース接続障害時の場合にエラーを返すことを検証(t *testing.T) {
}
例としてsumという関数のテストを書いてみます.
func sum(a, b int) int {
return a + b
}
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)
// スライスの要素が同じ事を確認、ただし順序を問わない
assert.ElementsMatch(t, expectedSlice, actualSlice)
ユニットテストの実行
goコマンドでユニットテストを実行できます.
$ go test calc_test.go
もしくは次のようにすることでカレントディレクトリ配下のすべてのテストを実行できます.
$ go test ./...
また,-v
オプションを付けることでテストの実行状態の詳細を出力できます.
go test -v calc_test.go
go test -v ./...
ここまでGo言語におけるユニットテストの基本でした.
テストしやすいコードの書き方
テストしやすいコードとは?
ユニットテストを積極的に導入するには予め本体コードをテストしやすいコードにしておく必要があります.そのためには以下の心がけが必要です.この心がけは言語を問わず言えることです.Go言語に限ったことではありません.
- 関数に外部依存の要素を作らない.
- 外部依存の要素は引数で注入できるようにする.
- 外部依存を含む関数はまとめる.
- 一つの関数でたくさんの処理をしない.
- これは保守性の高いコードを書く上で基本.
- モック化できるようにコードを書く
モック化できるコード
本記事では,Go言語において「モック化できるように書く」ことを記述します.
ユニットテスト時においてモック化とはテストに依存する外部依存の関数の動作をテストのためにふるまいを置き換えることを言います.
例えば,テスト対象の関数内でデータベースに接続してユーザ情報を取得する処理を行っている場合,モック化することでユニットテストでは実際にはデータベースに接続しませんが,接続した体で値を返してテストを進めると言ったことです.
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を指定します.
func (db *productDB) GetProduct(data *model.Product) {
data = &model.Product{}
}
この場合,以下のようにすることで引数に代入した振る舞いができます.
expectedProduct := &model.Product{}
dbMock.EXPECT().GetProduct.SetArg(0, expectedProduct)
まとめ
Go言語におけるユニットテストとユニットテストを書きやすくするための実装方法,モック化について書きました.
Tips
モックの生成時にエラーになったら..
モック生成後,元の構造体・インターフェイスを変更後に再度生成する際にエラーになる場合があります.
大抵は既存のコードと新しいコードで関係性が維持できない場合.
そんなときは,問題発生原因のコードをビルド対象から外すとモックが生成できるようになるかもしれません.以下の記述をソースコードの1行目に加えると外れます.
// +build test