昨日までPOSTメソッドとか言ってましたが実際にはrpc CreateTodo
ですね、失礼しました。
実装していきます。
実装していく過程で、テストやモックなどもみていきたいと思います。
では参りましょう。
インターフェイスの定義
まずインターフェイスを定義し、server構造体からどう呼び出したいかを決めます。
今回はTodoを作成したいので以下のように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
を作成し、テストを書いていきましょう。
package main
import "testing"
func TestServer_CreateTodo(t *testing.T) {
}
個人的にはTestSuite構造体を作ってテストを書くのがお気に入りです。
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,
}
}
まずは保存が成功するパターンをテストしてみましょう。
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()
を呼び出すことなので、以下のようにアサーションを追加します。
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も値を埋めていきましょう。
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構造体を更新してタイトルと説明文が保存されるようにします。
type (
todo struct {
title string
description string
}
...
)
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
を以下のように更新して、もう一度実行してみます。
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インターフェイスを実装する部分ですが、少し長くなってきたのでまた明日にします。では。