1
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

インターフェイス、テスト、モック

昨日までPOSTメソッドとか言ってましたが実際にはrpc CreateTodoですね、失礼しました。
実装していきます。

実装していく過程で、テストやモックなどもみていきたいと思います。
では参りましょう。

インターフェイスの定義

まずインターフェイスを定義し、server構造体からどう呼び出したいかを決めます。
今回はTodoを作成したいので以下のようにserver.goに定義しました。

server.go
...

var _ service.TodoAPIServer = (*server)(nil)

type (
    todo        struct{}
    todoManager interface {
        projectTodo(todo) (string, error)
    }

    server struct {
        todoMgr todoManager
    }
)

func (*server) GetTodo(context.Context, *service.GetTodoRequest) (*service.GetTodoResponse, error) {

...

todo構造体を受け取り、保存したらそのidを返します。
サーバーはこのtodoManagerインターフェイスへ依存することにします。

モックの作成

ではテストを書くために、このインターフェイスからモックを作成しましょう。
モックのコードジェネレートには github.com/golang/mock/mockgen を使います。
今回は直接Makefileにタスクを追加していきましょう。まずはツールインストール用のタスク。

mockgen-install:
    GO111MODULE=off go get github.com/golang/mock/gomock
    go install github.com/golang/mock/mockgen

次にコード生成用のタスク。

mockgen:
    mockgen -source=server.go -package=main -destination server_mock.go -mock_names todoManager=MockTodoManager

では試してみます。

$ make mockgen-install
GO111MODULE=off go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen

$ make mockgen
mockgen -source=server.go -package=main -destination server_mock.go -mock_names todoManager=MockTodoManager

無事server_mock.goが生成されているようです。

テスト

ではserver_test.goを作成し、テストを書いていきましょう。

server_test.go
package main

import "testing"

func TestServer_CreateTodo(t *testing.T) {
}

個人的にはTestSuite構造体を作ってテストを書くのがお気に入りです。

server_test.go
type serverTestSuite struct {
    sut     *server
    ctrl    *gomock.Controller
    todoMgr *MockTodoManager
}

func newServerTestSuite(t *testing.T) serverTestSuite {
    ctrl := gomock.NewController(t)
    todoMgr := NewMockTodoManager(ctrl)
    return serverTestSuite{
        sut:     &server{todoMgr: todoMgr},
        ctrl:    ctrl,
        todoMgr: todoMgr,
    }
}

まずは保存が成功するパターンをテストしてみましょう。

server_test.go
func TestServer_CreateTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        s := newServerTestSuite(t)
        defer s.ctrl.Finish()

        input := &service.CreateTodoRequest{}
        want := &service.CreateTodoResponse{}

        got, err := s.sut.CreateTodo(context.Background(), input)
        require.NoError(t, err)
        assert.Equal(t, want, got)
    })
}

ここで期待すべきはserver構造体がtodoMgr.projectTodo()を呼び出すことなので、以下のようにアサーションを追加します。

server_test.go
func TestServer_CreateTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        s := newServerTestSuite(t)
        defer s.ctrl.Finish()

        input := &service.CreateTodoRequest{}

        todoID := uuid.New().String()
        want := &service.CreateTodoResponse{}

        s.todoMgr.EXPECT().
            projectTodo(todo{}).
            Return(todoID, nil)

        got, err := s.sut.CreateTodo(context.Background(), input)
        require.NoError(t, err)
        assert.Equal(t, want, got)
    })
}

inputとwantも値を埋めていきましょう。

server_test.go
func TestServer_CreateTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        s := newServerTestSuite(t)
        defer s.ctrl.Finish()

        input := &service.CreateTodoRequest{
            Todo: &service.Todo{
                Title:       "foo todo",
                Description: "foo description",
            },
        }

        todoID := uuid.New().String()
        want := &service.CreateTodoResponse{
            Success: true,
            Id:      todoID,
        }

        s.todoMgr.EXPECT().
            projectTodo(todo{}).
            Return(todoID, nil)

        got, err := s.sut.CreateTodo(context.Background(), input)
        require.NoError(t, err)
        assert.Equal(t, want, got)
    })
}

最後にtodo構造体を更新してタイトルと説明文が保存されるようにします。

server.go
type (
    todo struct {
        title       string
        description string
    }
    ...
)
server_test.go
func TestServer_CreateTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        s := newServerTestSuite(t)
        defer s.ctrl.Finish()

        input := &service.CreateTodoRequest{
            Todo: &service.Todo{
                Title:       "foo todo",
                Description: "foo description",
            },
        }

        todoID := uuid.New().String()
        want := &service.CreateTodoResponse{
            Success: true,
            Id:      todoID,
        }

        s.todoMgr.EXPECT().
            projectTodo(todo{
                title:       input.Todo.Title,
                description: input.Todo.Description,
            }).
            Return(todoID, nil)

        got, err := s.sut.CreateTodo(context.Background(), input)
        require.NoError(t, err)
        assert.Equal(t, want, got)
    })
}

いい感じです。テストを実行してみます。

$ make test
go test -v -cover -timeout 30s ./...
=== RUN   TestServer_CreateTodo
=== RUN   TestServer_CreateTodo/project_a_new_todo
--- FAIL: TestServer_CreateTodo (0.00s)
    --- FAIL: TestServer_CreateTodo/project_a_new_todo (0.00s)
        server_test.go:57: 
                Error Trace:    server_test.go:57
                Error:          Not equal: 
                                expected: &service.CreateTodoResponse{Success: true,
                                Id: "1fbf35f0-9b75-4d28-95fe-ea0dc3d67866",
                                }
                                actual  : &service.CreateTodoResponse{Success: false,
                                Id: "",
                                }

                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1,2 +1,2 @@
                                -(*service.CreateTodoResponse)(&CreateTodoResponse{Success:true,Id:1fbf35f0-9b75-4d28-95fe-ea0dc3d67866,})
                                +(*service.CreateTodoResponse)(&CreateTodoResponse{Success:false,Id:,})

                Test:           TestServer_CreateTodo/project_a_new_todo
        server_test.go:58: missing call(s) to *main.MockTodoManager.projectTodo(is equal to {foo todo foo description}) /Users/kenta/.ghq/github.com/KentaKudo/qiita-advent-calendar-2019/server_test.go:49
        server_test.go:58: aborting test due to missing call(s)
FAIL
coverage: 9.1% of statements
FAIL    github.com/KentaKudo/qiita-advent-calendar-2019 0.682s
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service     [no test files]
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files]
FAIL
make: *** [test] Error 1

いいですね、期待通り失敗しています。

server.CreateTodoの実装

では、server.goを以下のように更新して、もう一度実行してみます。

server.go
func (s *server) CreateTodo(ctx context.Context, req *service.CreateTodoRequest) (*service.CreateTodoResponse, error) {
    id, err := s.todoMgr.projectTodo(todo{
        title:       req.Todo.Title,
        description: req.Todo.Description,
    })
    if err != nil {
        return nil, err
    }

    return &service.CreateTodoResponse{
        Success: true,
        Id:      id,
    }, nil
}
$ make test
go test -v -cover -timeout 30s ./...
=== RUN   TestServer_CreateTodo
=== RUN   TestServer_CreateTodo/project_a_new_todo
--- PASS: TestServer_CreateTodo (0.00s)
    --- PASS: TestServer_CreateTodo/project_a_new_todo (0.00s)
PASS
coverage: 17.4% of statements
ok      github.com/KentaKudo/qiita-advent-calendar-2019 0.695s  coverage: 17.4% of statements
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service     [no test files]
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files]

無事テストが通りました:)
todoMgrがエラーを返すパターンもテストしますが、目新しいことはないので説明は割愛します。コミットを確認してみてください。


残るはstore構造体にtodoManagerインターフェイスを実装する部分ですが、少し長くなってきたのでまた明日にします。では。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
1
Help us understand the problem. What are the problem?