1. hiroyky

    Posted

    hiroyky
Changes in title
+Go言語でユニットテスト, テストしやすいコードにモックを書く
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,253 @@
+# Go言語でテストしやすいコードを書く
+
+Go言語でテスタブル(テストしやすい)コードを書いてユニットテストを実践しましょう.
+この記事では以下について書きます.ユニットテストの概要については書きません.
+
+- Go言語でのユニットテストの書き方
+- テストアサーション
+- Go言語でのユニットテストの実行方法
+- テストしやすいコードの書き方
+- Go言語でのモックの作成とモックの使い方
+
+# Go言語でのユニットテストの書き方
+
+## テストコードのファイル(xxxx_test.go)
+Go言語では本体コードと同じパッケージ内(同じディレクトリ内)にテストファイルを作成するのが一般的のようです. ファイル名は``xxx_test.go``にするのがルールです.
+
+```
+├── calc.go
+└── calc_test.go
+```
+
+## テストコードの基本
+
+テストコードでは以下のパッケージを使います.
+
+- testing (必須)
+- [github.com/stretchr/testify/assert](https://godoc.org/github.com/stretchr/testify/assert) (アサーション用, 必須ではないがあると便利)
+
+関数は``TestXXXX``という名称で引数に``*testing.T``型を取る必要があります.
+関数名は日本語も含めることができます.テストコードではテスト項目をよりわかりやすくするために開発チームが最も理解できる言葉で書く場合があります(筆者個人としては,テストコードに限っては日本語関数名を勧めて言います.).
+
+```go
+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)
+}`
+```
+
+# ユニットテストの実行
+
+goコマンドでユニットテストを実行できます.
+
+```shell
+$ go test calc_test.go
+```
+
+もしくは次のようにすることでカレントディレクトリ配下のすべてのテストを実行できます.
+
+```shell
+$ go test ./....
+```
+
+また,``-v``オプションを付けることでテストの実行状態の詳細を出力できます.
+
+```shell
+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``を指しています,
+
+
+従って,呼び出し側は以下のようになります.
+
+```go
+db := NewUserDB()
+user, err := db.GetUser("12")
+```
+
+このときUserDBを外部注入できるようにしておけばテストしやすいコードを書くことができます.
+具体的には以下のようにします. この例ではインターフェイスUserDBに依存するUserServiceを記述しています.ポイントは構造体ではなくインターフェイスにしている点です.これのよりUserDBを置き換えるとができます.
+
+```go
+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](https://github.com/golang/mock)コマンドを使用します.
+以下のようにすることで,``user_db.go``のモックが``mocks/``配下に作成されます.最後の``package``オプションで生成するモックが属するパッケージを指定しています.
+
+```shell
+go get github.com/golang/mock/mockgen
+mockgen -soruce user_db.go -destination mocks/user_db.go -package mocks
+```
+
+## モックを使ったテストコード
+
+生成したモックは元とインターフェイスを持つので置き換えることができます.そしてモック特有の関数で振る舞いを変えることができます.
+
+```go
+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()...``の部分で,期待される呼び出し方とふるまいについて定義しています.ここで期待したとおりに関数が呼び出されなければテストエラーとなります.
+
+## モックの使い方
+モックの詳細な使い方については[公式リファレンス](https://godoc.org/github.com/golang/mock/gomock)を参照してください.
+ここでは,よく使うであろう使い方について記述します.
+
+### 引数チェック
+``EXPECT().関数名(期待したい値)``を記述します・「期待したい値」で呼び出しがなければテストエラーとなります.
+
+```go
+dbMock.EXPECT().GetUser("user-id1")
+```
+### 何でもいい引数
+引数ななんでも良い場合は,gomock.Any()を指定します.
+
+```go
+import "github.com/golang/mock/gomock"
+
+dbMock.EXPECT().GetUser(gomock.Any())
+```
+
+### 戻り値の振る舞い
+戻り値を指定したい場合はReturnで指定します.
+
+```go
+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{}
+}
+```
+
+この場合,以下のようにすることで引数に代入した振る舞いができます.
+
+```go
+expectedProduct := &model.Product{}
+dbMock.EXPECT().GetProduct.SetArg(0, expectedProduct)
+```
+
+# まとめ
+Go言語におけるユニットテストとユニットテストを書きやすくするための実装方法,モック化について書きました.
+
+# Tips
+## モックの生成時にエラーになったら..
+モック生成後,元の構造体・インターフェイスを変更後に再度生成する際にエラーになる場合があります.
+大抵は既存のコードと新しいコードで関係性が維持できない場合.
+
+そんなときは,問題発生原因のコードをビルド対象から外すとモックが生成できるようになるかもしれません.以下の記述をソースコードの1行目に加えると外れます.
+
+```go
+// +build test
+```