【テストコードの実装】Go言語でのAPI開発 | 実践的な学習から実装まで⑥
こんにちは!たけのこです。
今回も引き続き、Go言語を使用したAPIの実装方法について解説していきます。
前回は、データ更新APIの実装について解説してきました。
あらかた、APIの実装について解説してきましたが、
今回は実装してきたコードをテストする、テストコードの実装について解説していきます。
Go言語開発では、絶対と言ってもいいほど実装する分野になります。
では、早速いきましょう!!
Step 1:既存コードの修正
テストコードを実装する際に必要なインタフェースなどを、既存のコードに追加していきます。
まずは「user_repository.go」ファイルから修正していきましょう!
下記コードのコメントしているところに修正を加えています。
package repository
import (
"database/sql"
"myApiProject/internal/domain"
"strconv"
"strings"
)
// 下のインフェースを追加
type UserRepository interface {
FindByID(userID string) (*domain.User, error)
UpdateUser(userRes domain.User) (string, error)
GetUserDetails(userID string) (*domain.User, error)
}
// type名のプレフィックスを小文字に変更
type userRepository struct {
db *sql.DB
}
// returnのプレフィックスを小文字に変更
func NewUserRepository(db *sql.DB) UserRepository {
return &userRepository{db}
}
// 引数の型のプレフィックスを小文字に変更
func (r *userRepository) FindByID(userID string) (*domain.User, error) {
user := &domain.User{}
err := r.db.QueryRow("SELECT user_id, name, email, password FROM Users WHERE user_id = ?", userID).Scan(&user.UserID, &user.Name, &user.Email, &user.Password)
if err != nil {
return nil, err
}
return user, nil
}
// 引数の型のプレフィックスを小文字に変更
func (r *userRepository) UpdateUser(userRes domain.User) (string, error) {
updates := []string{}
params := []interface{}{}
message := []string{}
message = append(message, "ユーザーID'"+strconv.Itoa(userRes.UserID)+"'の")
if userRes.Name != "" {
updates = append(updates, "name = ?")
params = append(params, userRes.Name)
message = append(message, "名前を'"+userRes.Name+"'に、")
}
if userRes.Email != "" {
updates = append(updates, "email = ?")
params = append(params, userRes.Email)
message = append(message, "Emailを'"+userRes.Email+"'に、")
}
if userRes.Password != "" {
updates = append(updates, "password = ?")
params = append(params, userRes.Password)
message = append(message, "パスワードを'"+userRes.Password+"'に、")
}
message = append(message, "変更しました!")
if len(updates) == 0 {
return "ユーザーデータの更新は0件数です!", nil
}
query := "UPDATE Users SET " + strings.Join(updates, ", ") + " WHERE user_id = ?"
params = append(params, userRes.UserID)
_, err := r.db.Exec(query, params...)
return strings.Join(message, ""), err
}
// 引数の型のプレフィックスを小文字に変更
func (repo *userRepository) GetUserDetails(userID string) (*domain.User, error) {
query := `
SELECT u.user_id, u.name, u.email, u.password,
o.order_id, o.quantity, o.order_date,
p.product_id, p.name, p.description, p.price
FROM Users u
LEFT JOIN Orders o ON u.user_id = o.user_id
LEFT JOIN Products p ON o.product_id = p.product_id
WHERE u.user_id = ?`
rows, err := repo.db.Query(query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var user domain.User
var orders []domain.Order
for rows.Next() {
var order domain.Order
var product domain.Product
if err := rows.Scan(&user.UserID, &user.Name, &user.Email, &user.Password,
&order.OrderID, &order.Quantity, &order.OrderDate,
&product.ProductID, &product.Name, &product.Description, &product.Price); err != nil {
return nil, err
}
order.Product = product
orders = append(orders, order)
}
user.Orders = orders
return &user, nil
}
今回テストコードを実装するにあたり、新たにrepositoryパッケージのコードに各メソッドのインタフェースを追加しています。
それに伴い、既に実装していた「UserRepositoryをuserRepository」に名前を変更しました。
[chat face=”cropped-24a6ce9ea80cd88a9664fbd22d097884.png” name=”たけのこ” align=”left” border=”green” bg=”none” style=”maru”] ちょっと分かりにくいですが、先頭の”U”を大文字から小文字に変更しています! [/chat]
Step 2:モックの生成
次に、テストコードで使用するモックを生成していきます。
今回はmockgenというコードを使用してモックの生成を行なっていきます。
実行するコマンドは下記のとおりです。
mockgen -source=自分のプロジェクトまでの絶対パス\myApiProject\internal\repository\user_repository.go -destination=自分のプロジェクトまでの絶対パス\myApiProject\internal\repository\mock_user_repository.go -package=repository
実行すると、下記のような「mock_user_repository.go」ファイルが生成されます。
// Code generated by MockGen. DO NOT EDIT.
// Package repository is a generated GoMock package.
package repository
import (
domain "myApiProject/internal/domain"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockUserRepository is a mock of UserRepository interface.
type MockUserRepository struct {
ctrl *gomock.Controller
recorder *MockUserRepositoryMockRecorder
}
// MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository.
type MockUserRepositoryMockRecorder struct {
mock *MockUserRepository
}
// NewMockUserRepository creates a new mock instance.
func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository {
mock := &MockUserRepository{ctrl: ctrl}
mock.recorder = &MockUserRepositoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder {
return m.recorder
}
// FindByID mocks base method.
func (m *MockUserRepository) FindByID(userID string) (*domain.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindByID", userID)
ret0, _ := ret[0].(*domain.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindByID indicates an expected call of FindByID.
func (mr *MockUserRepositoryMockRecorder) FindByID(userID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByID", reflect.TypeOf((*MockUserRepository)(nil).FindByID), userID)
}
// GetUserDetails mocks base method.
func (m *MockUserRepository) GetUserDetails(userID string) (*domain.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserDetails", userID)
ret0, _ := ret[0].(*domain.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserDetails indicates an expected call of GetUserDetails.
func (mr *MockUserRepositoryMockRecorder) GetUserDetails(userID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserDetails", reflect.TypeOf((*MockUserRepository)(nil).GetUserDetails), userID)
}
// UpdateUser mocks base method.
func (m *MockUserRepository) UpdateUser(userRes domain.User) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", userRes)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockUserRepositoryMockRecorder) UpdateUser(userRes interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockUserRepository)(nil).UpdateUser), userRes)
}
こちらの生成されたモックファイルは、そのまま使えるので誤って編集しないように気を付けましょう!
Step 3:テストコードの実装
テストコードの実装準備が完了しましたので、早速実装していきましょう!
今回はAPIの根幹ともいえる、handlerパッケージのテストコードを実装していきます。
新しく、handlerパッケージ内にtestパッケージを作成して、「user_handler_test.go」ファイルを作成します。
// handlerパッケージのフォルダ構成
internal
┗//~いろんなパッケージ(省略)~
┗handler
┗user_handler.go
┗test
┗user_handler_test.go
作成したuser_handler_test.goファイルに下記コードを記載していきます。
また、今回はエラー出力テストなどは実装せずに正常通りに動くことのみ確認してみます!
package handler
import (
"myApiProject/internal/domain"
"myApiProject/internal/handler"
"myApiProject/internal/repository"
"myApiProject/internal/usecase"
"net/http"
"net/http/httptest"
"testing"
"github.com/golang/mock/gomock"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func TestGetUser(t *testing.T) {
// モックコントローラを作成
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// モックリポジトリを生成
mockRepo := repository.NewMockUserRepository(ctrl)
userUsecase := usecase.NewUserUseCase(mockRepo)
userHandler := handler.NewUserHandler(userUsecase)
// ルーターを設定
r := mux.NewRouter()
r.HandleFunc("/user", userHandler.GetUser).Methods("GET")
// テストデータを作成
testUser := &domain.User{
UserID: 1,
Name: "山田太郎",
Email: "taro@example.com",
Password: "password123",
}
// テスト用のHTTPリクエストを作成
req, _ := http.NewRequest("GET", "/user?user_id=1", nil)
rr := httptest.NewRecorder()
// モックの期待値を設定
mockRepo.EXPECT().FindByID("1").Return(testUser, nil)
// リクエストを処理
r.ServeHTTP(rr, req)
// レスポンスのアサーション
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `{"user_id":1,"name":"山田太郎","email":"taro@example.com","password":"password123","orders":null}`, rr.Body.String())
}
正常動作テストはこんな感じですね。
パッと見ても、なんのこっちゃ分からないと思いますので、
順番に解説していきます!
モックコントローラーの作成
// モックコントローラを作成
ctrl := gomock.NewController(t)
defer ctrl.Finish()
こちらでは、モックコントローラーの作成とテスト終了時にリソースを解放する処理を記載しています。
モックコントローラーの作成は、テスト実施の準備のようなイメージを持つと分かりやすいかもしれません。
また、リソースの解放についてはdefer句で処理の最後に呼ばれるよう設定しているので
テストコードが全て実行されたら、テストに使用したリソースの解放が行われるようにしています。
モックリポジトリ、ユースケース、ハンドラの初期化
// モックリポジトリを生成
mockRepo := repository.NewMockUserRepository(ctrl)
userUsecase := usecase.NewUserUseCase(mockRepo)
userHandler := handler.NewUserHandler(userUsecase)
こちらでは、先ほど生成したモックを使用してユースケースとハンドラーの初期化を行なっています。
こちらもテスト実施の準備になります!
ルーターのセットアップ
// ルーターを設定
r := mux.NewRouter()
r.HandleFunc("/user", userHandler.GetUser).Methods("GET")
こちらでは、ハンドラーのGetUserを呼び出す際のエンドポイントの設定を行なっています。
今回は、ユーザーデータを単純に取得するAPIのテストコードなので「GET」メソッドを指定してAPIを呼び出しています。
[chat face=”cropped-24a6ce9ea80cd88a9664fbd22d097884.png” name=”たけのこ” align=”left” border=”green” bg=”none” style=”maru”] ユーザーデータ更新APIのテストコードの場合は、「Methods(“POST”)」という書き方になります! [/chat]
テストデータの作成
// テストデータを作成
testUser := &domain.User{
UserID: 1,
Name: "山田太郎",
Email: "taro@example.com",
Password: "password123",
}
こちらでは、「user_handler.go」ファイルでも実際に呼び出している「FindByID」メソッドを呼び出した際に想定される戻り値データを作成しています。
今回はFindByIDメソッドに「user_id=1」をセットして呼び出すため、
「上記データが返答されるだろうなぁ~」
といった想定でデータを準備しています。
HTTPリクエストの作成
// テスト用のHTTPリクエストを作成
req, _ := http.NewRequest("GET", "/user?user_id=1", nil)
rr := httptest.NewRecorder()
テスト用のHTTP GETリクエストを作成し、呼び出した際に返答されるデータを「rr」に格納するような設定を行なっています。
「ルーターのセットアップ」という部分でも似たようなことを行なっているように思えますが、あちらではハンドラーを呼び出す際のルーティングの設定。こちらでは、テスト用のHTTPリクエストを作成してセットアップしたルーターを通して、APIが期待通りに動くのかの確認をしています。
モックの期待値設定
// モックの期待値を設定
mockRepo.EXPECT().FindByID("1").Return(testUser, nil)
こちらは、モックリポジトリのFindByIDメソッドを呼び出し、戻り値にtestUserにセットしていたデータがしっかりレスポンスされるのかを検証しています。
どちらかと言うと、ハンドラーのテストというよりリポジトリパッケージの「FindByID」が期待通りの値を返答するのかのチェックをしています!
リクエストの送信とレスポンスの確認
// リクエストを処理
r.ServeHTTP(rr, req)
// レスポンスのアサーション
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `{"user_id":1,"name":"山田太郎","email":"taro@example.com","password":"password123","orders":null}`, rr.Body.String())
最後に、レスポンスされた内容のステータスチェックと
データが期待値通りのものかをチェックしています。
「モックの所でもデータのチェックをしたような。。。?」
と思うかもしれませんが、
- モックで検証したのはFindByIDメソッドから返答されたデータ検証
- assert.JSONEq()では、API使用者にレスポンスされるデータ検証
という違いがあります。
まぁ、JSON形式に成形したかどうかの違いくらいしかないので、
そこまで大きな違いはありませんが、ちょっとだけデータに装飾を加えるAPIの場合には
最後の検証部分は一番重要になってくるところかもしれませんね!
まとめ
ということで、
今回はテストコードの実装についてまとめました。
僕もテストコードを実装したことによって、
大量のバグを事前に潰すことができた経験がありますので
Go言語の中でも一番と言ってもいいほど、重要な部分だと思っています。
こちらの記事が、Go言語勉強中の人の糧になれば幸いです。
というわけで、今回はこの辺りで!ありがとうございました!!