前提
- CleanArchitecture を採用する
- CleanArchitecture のusecase 層のテストをする
- repository 層はgomockによってモックが生成されている
- テストの雛形はVSCode の拡張のgotests の形に基づく参考: VS CodeのGo言語テストコード生成ツールを使ってみたらめちゃくちゃ便利だった話とか
概要
要件としてrepository 層の関数をモックとして扱う必要がある。
gotests で雛形を生成してgomock を用いつつ引数としてmockの初期化関数を渡してモックパターンを柔軟に設定する
参考にも記載したのですが上記サイトがgomock を使う上で大変参考になりました。思想はこの記事から引き継いでいます。
例:テストしたい関数
type UserUsecase interface {
Get(ctx context.Context, id int) (*User, error)
}
type userUsecase struct {
repo repository.UserRepository
}
func (uc *userUsecase) Get(ctx context.Context, id int) (*User, error) {
// サンプル用バリデーション: id が負の数だった場合はエラーを返す
if id < 1 {
return nil, errors.New("validation error")
}
u, err := uc.repo.Get(ctx, userID)
if err != nil {
return nil, err
}
return u, nil
}
//go:generate mockgen -source=$GOFILE -destination=../mock/$GOFILE -package=mock
type UserRepository interface {
Get(ctx context.Context, id int) (*User, error)
}
type userRepository struct {
db *sql.DB
}
func (repo *userRepository) Get(ctx context.Context, id int) (*User, error) {
return // SELECT * FROM users WHERE id = 1
}
repository のインターフェースに依存したusecase で
雑なバリデーション込のユーザーをGETする簡単な関数についてのテストを考えます。
なおrepository には go generate
の記述を記載しており、このコメントにより go generate ./...
をホームディレクトリで行うことにより該当ディレクトリのmockディレクトリにmockを自動で生成してくれます。
初期化部分やrepository のSQL部分、細かい定義などは便宜上省略しました。
テストコード
func Test_userUsecase_Get(t *testing.T) {
type args struct {
ctx context.Context
params *user.GetParams
}
tests := []struct {
name string
prepareMockFn func(mu *mockUser.MockUserRepository)
args args
want *entity.User
wantErr bool
}{
{
name: "ユーザーID=1が渡された場合はID=1のユーザーエンティティが返る",
prepareMockFn: func(mu *mockUser.MockUserRepository) {
mu.EXPECT().Get(gomock.Any(), "firebase_uid").Return(User{ID: 1, name: "hoge"}, nil)
},
args: args{
ctx: context.Background(),
userID: 1
},
},
want: User{ID: 1, name: "hoge"},
wantErr: false,
},
{
name: "存在しないユーザーIDが渡された場合はエラーを返す",
prepareMockFn: func(mu *mockUser.MockUserRepository) {
mu.EXPECT().Get(gomock.Any(), 999).Return(nil, errors.New("hoge"))
},
args: args{
ctx: context.Background(),
userID: 999
},
},
want: nil,
wantErr: true,
},
{
name: "負の数のIDが渡された場合はバリデーションでエラーが返る",
prepareMockFn: func(mu *mockUser.MockUserRepository) {
mu.EXPECT().Get(gomock.Any(),gomock.Any()).Times(0)
},
args: args{
ctx: context.Background(),
userID: -1
},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mu := mockUser.NewMockUserRepository(ctrl)
tt.prepareMockFn(mu)
uc := NewUserUsecase(mu)
got, err := uc.Get(tt.args.ctx, tt.args.params)
if (err != nil) != tt.wantErr {
t.Errorf("userUsecase.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("userUsecase.Get() = %v, want %v", got, tt.want)
}
})
}
}
以上のようにprepareMockFn という引数を与えることによって、テストケースごとに柔軟にモックの動きを調整することができます。
参考
参考にしたサイトがあったなーと思って引っ張ってきたのですが、ほぼ新規性なく同じことを僕が真似していただけでした。
こちらはじめに参考にしていただけるとより上記コードの理解も深まると思います。
議論・疑問
テストを通して自分がまだはっきりとしていない部分を備忘録として記載しておきます。
- 議論としてレポジトリのmockをどう初期化するか
- 全体共通化してもよい
- fixtures などのダミーデータを返す設定を定義をどこにどう書くか
- usecase 自体の初期化をテスト内でどう表現するか
- gomock のEmbedding した場合の自動生成方法(gomock issue #85にあるようにTODOとされている)