LoginSignup
3
1

More than 1 year has passed since last update.

gomock とgotests を使ったCleanArchitectureのusecaseテスト生成術

Last updated at Posted at 2022-02-05

前提

概要

要件としてrepository 層の関数をモックとして扱う必要がある。
gotests で雛形を生成してgomock を用いつつ引数としてmockの初期化関数を渡してモックパターンを柔軟に設定する

参考にも記載したのですが上記サイトがgomock を使う上で大変参考になりました。思想はこの記事から引き継いでいます。

例:テストしたい関数

usecase.go
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
}
repository.go
//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部分、細かい定義などは便宜上省略しました。

テストコード

usecase_test.go
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とされている)
3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1