はじめまして!フリーランスエンジニアのこたろうです。
HTTPハンドラのテストとモック実装について、学びで得た知見を共有します。
なぜインターフェースを使用するのか
Webアプリケーションのテストにおいて、最も重要な点の1つは「依存関係の分離」です。例えば、記事を取得するAPIのテストを考えてみましょう。このAPIは通常、データベースからデータを取得しますが、テストのたびにデータベースを準備するのは非効率です。
ここで威力を発揮するのが、インターフェースによる抽象化です。以下のように、サービス層をインターフェースとして定義することで、テスト時に実装を自由に差し替えることができます:
// インターフェース定義
type ArticleServicer interface {
FindAll() ([]Article, error)
FindByID(id int) (*Article, error)
}
// 本番用の実装
type ArticleService struct {
db *sql.DB
}
// テスト用のモック実装
type MockArticleService struct {
articles []Article
}
これにより、以下のような利点が得られます:
- データベースに依存しないテストが可能になる
- テストのセットアップが簡単になる
- テストの実行が高速になる
- 様々なエラーケースのテストが容易になる
HTTPハンドラのテスト方法
Goには、HTTPハンドラをテストするための強力なツールが用意されています。特に重要なのがhttptest
パッケージです。
テストリクエストの作成
httptest.NewRequest
関数を使用すると、実際のHTTP通信なしでリクエストを作成できます:
// GETリクエストの作成
req := httptest.NewRequest("GET", "/articles/1", nil)
// POSTリクエストの作成(JSONボディ付き)
body := strings.NewReader(`{"title": "Test Article"}`)
req := httptest.NewRequest("POST", "/articles", body)
レスポンスの記録と検証
httptest.NewRecorder
は、ハンドラが生成したレスポンスを記録するための特別なResponseWriter
を提供します:
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
// ステータスコードの検証
if rec.Code != http.StatusOK {
t.Errorf("expected status OK; got %v", rec.Code)
}
// レスポンスボディの検証
var response Article
json.NewDecoder(rec.Body).Decode(&response)
gorilla/muxを使用したルーティングのテスト
gorilla/mux
を使用している場合、URLパラメータの取り扱いが重要になります:
func TestArticleHandler_WithMux(t *testing.T) {
// ルーターの設定
router := mux.NewRouter()
mockService := &MockArticleService{
articles: []Article{{ID: 1, Title: "Test Article"}},
}
handler := NewArticleHandler(mockService)
// パスパラメータを使用するルートの登録
router.HandleFunc("/articles/{id}", handler.GetArticle).Methods("GET")
// テストサーバーの作成
ts := httptest.NewServer(router)
defer ts.Close()
// リクエストの実行
resp, err := http.Get(fmt.Sprintf("%s/articles/1", ts.URL))
if err != nil {
t.Fatal(err)
}
// レスポンスの検証
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status OK; got %v", resp.StatusCode)
}
}
このテストでは、実際のサーバーと同じようにルーティングが機能することを確認できます。gorilla/mux
は、URLパラメータを自動的に解析し、ハンドラに渡してくれます。
モックサービスの実装例
テストでは、本番のデータベース操作を模倣したモックサービスを使用します:
type MockArticleService struct {
articles []Article
err error // テスト用にエラーを注入できる
}
func (m *MockArticleService) FindByID(id int) (*Article, error) {
if m.err != nil {
return nil, m.err
}
for _, article := range m.articles {
if article.ID == id {
return &article, nil
}
}
return nil, errors.New("article not found")
}
このモックは:
- テストケースに応じてレスポンスをカスタマイズ可能
- エラーケースのテストが容易
- データベース操作なしで高速にテスト可能
テストの実践的なポイント
-
テストケースの網羅
- 正常系(成功)ケース
- エラーケース(不正なID、データベースエラーなど)
- バリデーションエラー
- 空のレスポンスケース
-
レスポンスの詳細な検証
- ステータスコード
- レスポンスヘッダー
- レスポンスボディの構造と内容
- エラーメッセージの形式
-
エッジケースのテスト
- 不正なJSONリクエスト
- 存在しないリソース
- 大きなペイロード
- 特殊文字を含むパラメータ
参考文献:
-『Goで作るはじめてのWebアプリケーション改訂版』技術評論社