🐎

Go - ユニテ環境を爆速で導入!

2022/01/13に公開

「Goアプリにユニットテスト環境を爆速で導入し爆速で実装!」たったの3ステップで!

さっそく

Step1 "gomockをインストール!"

以下のコマンドを実行しましょう(今回は v1.6.0 を使います)

% go install github.com/golang/mock/mockgen@v1.6.0

Step2 "テストコードを(ある程度)自動生成!"

以下のコマンドを実行しましょう(user_repository.goがあるとします)

% mockgen -source=user_repository.go -destination=mock_user_repository.go

以下のコマンドを実行しましょう(user_find_usecase.goがあるとします)

% gotests -w -all user_find_usecase.go

参考

user_repository.go
package main

type UserRepository interface {
    FindById(id uint) (*User, error)
}
user_find_usecase.go
package main

type UserFindUseCase interface {
    FindById(id uint) (*User, error)
}

type userFindUseCase struct {
    userRepository UserRepository
}

func (r *userFindUseCase) FindById(id uint) (*User, error) {
    ...
}

Step3 "テストケースを追加!"

Step2のgotests ...で生成されたコードを見てみましょう。(一部抜粋)

user_find_usecase_test.go
func Test_userFindUseCase_FindById(t *testing.T) {
    type fields strcut { userRepository UserRepository }
    type args struct { id uint }
    tests := []struct {
        name   string
	fields fields
	args   args
	want   (*User, error)
    }{
        // TODO: Add test cases.
    }
    for _, tt := range test {
        t.Run(tt.name, func(t *testing.T) {
	    u := &userFindUseCase{
	        userRepository: tt.fields.userRepository,
	    }
	    if got := u.FindById(tt.args.id); !reflect.DeepEqual(got, tt.want) {
	        t.Errorf("userFindUseCase.FindById() = %v, want %v", got, tt.want)
	    }
	})
    }
}

上記が自動生成されたコードです。
// TODO: Add test cases.と書かれた部分にテストケースの追加と
モックの振る舞いを定義すればテストコードが完成します。やってみましょう。

user_find_usecase_test.go
func Test_userFindUseCase_FindById(t *testing.T) {
    type fields strcut { userRepository UserRepository }
    type args struct { id uint }
    tests := []struct {
        name   string
	fields fields
	args   args
+	mocks  func(ur *MockUserRepository)
	want   (*User, error)
    }{
+	{
+	    name:  "[case1] success - get user",
+	    args:  args{
+	        id: uint(1)
+	    },
+	    mocks: func(ur *MockUserRepository) {
+	        ur.EXPECT().FindById(uint(1)).Return(&User{id: uint(1)}, nil)
+	    },
+	    want: &User{id: uint(1)}
+	},
    }
    for _, tt := range test {
        t.Run(tt.name, func(t *testing.T) {
+	    ctrl := gomock.NewController(t)
+	    defer ctrl.Finish()
	    
+	    ur := NewMockUserRepository(ctrl)
+	    tt.mocks(ur)
	    
	    u := &userFindUseCase{
+	        userRepository: ur,
	    }
	    if got := u.FindById(tt.args.id); !reflect.DeepEqual(got, tt.want) {
	        t.Errorf("userFindUseCase.FindById() = %v, want %v", got, tt.want)
	    }
	})
    }
}

上記が、自動生成されたコードに追記・編集したものです。
コード見るだけでなんとなく雰囲気は掴めると思います。(詳しくはこのあと解説します)

これでテストコード完成です。

ちょっとくわしく

いかがでしたか。爆速で導入&実装できたと思います。
詳しく見ていきましょう。

使用しているライブラリ

library version
gomock 1.6.0
cweill/gotests 1.6.0

gomockは、名前の通りモック生成してくれるライブラリです。
interface定義されたファイルを指定すると、そのモックを自動生成してくれます。
(step2のmockgenコマンドでやっていたこと)

cweill/gotestsは、テストコードの実装をサポートしてくれるライブラリです。
テストコードは公式推奨のTableDrivenTestsで生成してくれるため、
我々はテストケースの列挙だけすればOK、という便利なものです。
(step2のgotestsコマンドでやっていたこと)

step3でやっていること

追記・編集した箇所を中心に解説します。

mocks  func(ur *MockUserRepository)

モックの振る舞いを定義するために、テストケースの構造体に新しくフィールドを追加しています。

{
    name:  "[case1] success - get user",
    args:  args{
	id: uint(1)
    },
    mocks: func(ur *MockUserRepository) {
	ur.EXPECT().FindById(uint(1)).Return(&User{id: uint(1)}, nil)
    },
    want: (&User{id: uint(1)}, nil)
},

これがテストケースの定義です。nameがテスト名、argsがテストパラメータ、mocksがモックの振る舞い、wantが期待値になります。

ctrl := gomock.NewController(t)
defer ctrl.Finish()

ur := NewMockUserRepository(ctrl)
tt.mocks(ur)

モック呼び出しの管理をするControllerを用意し、実際に必要なモックを生成しています。
NewMockUserRepository()mockgenコマンドで自動生成されたものです。

u := &userFindUseCase{
userRepository: ur,
}

生成したモックをインジェクションしています。

Tips

「実装修正したのにmockコードの更新忘れてたからテストが落ちてる!」
こういうことはGithub ActionsなどのCIで更新させればいい感じになりそうですね。

最後に

ユニットテストは大事ですがその分実装負担もそれなりにありますよね。
でもこれらを使いこなすことである程度の負担軽減ができたかな〜と思います。

※サンプルコードにミスがあればご指摘いただけると幸いです。

Discussion