Go言語のhttptestパッケージを活用したHTTPハンドラテスト徹底解説

Go言語のhttptestパッケージは、HTTPハンドラのテストを効率的かつ簡単に行うための強力なツールです。HTTPハンドラは、Webアプリケーションのリクエストを受け取りレスポンスを返す重要な役割を担っていますが、その動作が期待通りかを確実にするにはテストが欠かせません。本記事では、httptestを活用したHTTPハンドラのテスト方法を具体的なコード例とともにわかりやすく解説します。これにより、開発者はハンドラの品質を高め、安定したアプリケーションを構築するための知識を身につけることができます。

目次
  1. Go言語におけるHTTPハンドラの基本概念
    1. HTTPハンドラの基本構造
    2. HTTPハンドラの役割
    3. HTTPハンドラのテストの重要性
  2. `httptest`パッケージの概要
    1. `httptest`の主な機能
    2. なぜ`httptest`を使用するのか
    3. 基本的な使い方
  3. `httptest.NewRecorder`の活用法
    1. モックレコーダの役割
    2. `httptest.NewRecorder`の基本的な使い方
    3. レスポンスヘッダーの検証
    4. レスポンスボディの検証
    5. 複雑なレスポンスの検証
  4. HTTPリクエストのモック作成方法
    1. モックリクエストの基本
    2. URLパラメータのモック
    3. リクエストヘッダーの設定
    4. リクエストボディのモック
    5. 複雑なリクエストのモック
    6. モックリクエストの応用
  5. ハンドラテストの実践例:基本編
    1. 基本的なHTTPハンドラのテスト
    2. ハンドラのパスパラメータを含むテスト
    3. ハンドラでエラーを返す場合のテスト
    4. リクエストヘッダーの処理を含むハンドラ
    5. まとめ
  6. ハンドラテストの実践例:応用編
    1. コンテキスト情報を利用するハンドラのテスト
    2. 外部サービスとの連携を模倣する
    3. ミドルウェアを含むハンドラのテスト
    4. 非同期処理を含むハンドラのテスト
    5. まとめ
  7. テストのベストプラクティス
    1. 1. モジュール化されたテストコード
    2. 2. 明確な期待値を設定
    3. 3. 異常系のテストを重視
    4. 4. テストデータの準備を工夫する
    5. 5. パラメータ化されたテスト
    6. 6. 継続的インテグレーション(CI)での自動実行
    7. まとめ
  8. 他のテストライブラリとの併用
    1. 1. `testify`との併用
    2. 2. `gomock`を使用したモック生成
    3. 3. `httpexpect`を使用したE2Eテスト
    4. 4. `go-cmp`による構造体比較
    5. 5. `ginkgo`と`gomega`によるBDDスタイルのテスト
    6. まとめ
  9. まとめ

Go言語におけるHTTPハンドラの基本概念


Go言語においてHTTPハンドラは、http.Handlerインターフェースを実装することでリクエストを処理し、レスポンスを生成する役割を果たします。HTTPサーバーは、受信したリクエストを適切なハンドラにルーティングし、クライアントに応答を返します。

HTTPハンドラの基本構造


Goでは、HTTPハンドラを簡単に定義できます。一般的には、http.Handlerインターフェースを実装する方法と、http.HandlerFunc型を使用する方法の2つがあります。

`http.Handler`の実装

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
}

`http.HandlerFunc`の使用

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
})

HTTPハンドラの役割

  1. リクエストの処理:リクエストパスやHTTPメソッドに基づいて、適切なロジックを実行します。
  2. レスポンスの生成:クライアントに返すデータを準備し、レスポンスヘッダやステータスコードを設定します。
  3. エラーハンドリング:リクエスト処理中に発生するエラーを管理し、ユーザーに適切なメッセージを返します。

HTTPハンドラのテストの重要性


HTTPハンドラは、Webアプリケーションの中核を担うコンポーネントです。そのため、ハンドラの動作が期待通りであることを確かめることが、アプリケーションの品質を保証する上で不可欠です。適切なテストを行うことで、次のような利点が得られます:

  • バグの早期発見
  • 新しい機能の安全な追加
  • コードの保守性向上

これらを踏まえ、httptestパッケージを活用することで、HTTPハンドラのテストがどのように簡単になるかを次章で解説します。

`httptest`パッケージの概要


Go言語の標準ライブラリに含まれるhttptestパッケージは、HTTPハンドラのテストを簡単かつ効率的に行うためのツールを提供します。このパッケージを使用すると、実際のサーバーを立ち上げることなく、HTTPリクエストやレスポンスをモックし、ハンドラの動作を検証できます。

`httptest`の主な機能


httptestパッケージは、以下の主要な機能を提供します:

1. `httptest.NewRecorder`


HTTPレスポンスを記録するモックレコーダを作成します。これにより、レスポンスのステータスコードやボディを簡単に検証できます。

2. `httptest.NewRequest`


HTTPリクエストをモックするための便利な関数を提供します。これにより、特定のURLやヘッダーを含むリクエストを簡単に作成できます。

3. サーバーのモック


一時的なHTTPサーバーを作成し、実際のネットワーク通信をシミュレートできます。

なぜ`httptest`を使用するのか


httptestを使うことで、HTTPハンドラのテストが以下のように効率化されます:

  • 環境の簡素化:テスト専用のモック環境を構築するため、本物のサーバーを立ち上げる必要がありません。
  • 素早い実行:テスト速度が向上し、頻繁なテストの実行が可能です。
  • テストの一貫性:モックされた環境は再現性が高いため、同じテスト結果を何度でも得ることができます。

基本的な使い方


以下に、httptestを用いた簡単な例を示します:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHelloHandler(t *testing.T) {
    // テスト対象のHTTPハンドラ
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    // モックリクエストとレスポンスレコーダを作成
    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()

    // ハンドラを呼び出し
    handler.ServeHTTP(recorder, req)

    // レスポンスの検証
    if recorder.Body.String() != "Hello, World!" {
        t.Errorf("expected Hello, World!, got %s", recorder.Body.String())
    }
}

次章では、httptest.NewRecorderの詳細な活用法を解説します。

`httptest.NewRecorder`の活用法


httptest.NewRecorderは、HTTPレスポンスを記録するためのモックオブジェクトを作成するための機能です。このオブジェクトを使うことで、ハンドラが生成するレスポンスを簡単に検証できます。httptest.NewRecorderは、GoでのHTTPハンドラテストにおいて最もよく使用されるツールの一つです。

モックレコーダの役割


httptest.NewRecorderが提供する主な役割は以下の通りです:

  1. レスポンスのキャプチャ:レスポンスのステータスコード、ヘッダー、ボディを記録します。
  2. テストの検証:記録したデータを使用して、期待される結果と一致するかを確認できます。
  3. 再現性のある環境:実際のHTTPレスポンスオブジェクトを使用せずに、テスト用のモック環境を構築できます。

`httptest.NewRecorder`の基本的な使い方


以下は、httptest.NewRecorderを使用したテストの基本的な例です:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestStatusCode(t *testing.T) {
    // テスト対象のHTTPハンドラ
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    })

    // レコーダとリクエストを作成
    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()

    // ハンドラを呼び出し
    handler.ServeHTTP(recorder, req)

    // ステータスコードの検証
    if recorder.Code != http.StatusOK {
        t.Errorf("expected status %v, got %v", http.StatusOK, recorder.Code)
    }
}

レスポンスヘッダーの検証


httptest.NewRecorderは、レスポンスヘッダーの内容も記録します。以下はヘッダーを検証する例です:

func TestResponseHeaders(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"message": "hello"}`))
    })

    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()

    handler.ServeHTTP(recorder, req)

    if recorder.Header().Get("Content-Type") != "application/json" {
        t.Errorf("expected Content-Type application/json, got %v", recorder.Header().Get("Content-Type"))
    }
}

レスポンスボディの検証


レスポンスの内容が期待通りかを確認するには、recorder.Body.String()を使用します:

func TestResponseBody(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()

    handler.ServeHTTP(recorder, req)

    if recorder.Body.String() != "Hello, World!" {
        t.Errorf("expected Hello, World!, got %v", recorder.Body.String())
    }
}

複雑なレスポンスの検証


複雑なレスポンスを検証する場合、レスポンスヘッダーやボディ、ステータスコードを組み合わせて確認することで、テストの精度を高めることができます。

次章では、モックリクエストの作成方法について詳しく解説します。

HTTPリクエストのモック作成方法


httptest.NewRequestは、HTTPリクエストをモックするための便利な関数です。この機能を利用すると、テスト用のHTTPリクエストを簡単に作成でき、HTTPハンドラの動作をシミュレートできます。本章では、httptest.NewRequestの使い方とその応用例について解説します。

モックリクエストの基本


httptest.NewRequestを使用すると、以下のパラメータを指定してモックリクエストを作成できます:

  • HTTPメソッド(例: GET, POST
  • URLパス(例: /api/test
  • リクエストボディ(必要に応じて設定)

以下は、基本的なモックリクエストの作成例です:

package main

import (
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestMockRequest(t *testing.T) {
    req := httptest.NewRequest("POST", "/api/test", strings.NewReader(`{"key": "value"}`))
    if req.Method != "POST" {
        t.Errorf("expected method POST, got %s", req.Method)
    }
}

URLパラメータのモック


モックリクエストでURLパラメータを設定する場合、クエリストリングをURLに含めるだけで対応可能です:

func TestQueryParameters(t *testing.T) {
    req := httptest.NewRequest("GET", "/api/test?key=value", nil)
    if req.URL.Query().Get("key") != "value" {
        t.Errorf("expected query parameter 'value', got %s", req.URL.Query().Get("key"))
    }
}

リクエストヘッダーの設定


req.Header.Setを使用して、リクエストヘッダーをモックできます:

func TestRequestHeaders(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    req.Header.Set("Content-Type", "application/json")

    if req.Header.Get("Content-Type") != "application/json" {
        t.Errorf("expected header Content-Type application/json, got %s", req.Header.Get("Content-Type"))
    }
}

リクエストボディのモック


httptest.NewRequestの第3引数を設定することで、リクエストボディを簡単に模倣できます:

func TestRequestBody(t *testing.T) {
    body := `{"message": "hello"}`
    req := httptest.NewRequest("POST", "/api/test", strings.NewReader(body))

    buf := new(strings.Builder)
    buf.ReadFrom(req.Body)

    if buf.String() != body {
        t.Errorf("expected body %s, got %s", body, buf.String())
    }
}

複雑なリクエストのモック


複雑なリクエストをモックする場合でも、httptest.NewRequestは十分に対応できます。以下は、複数のヘッダーとボディを設定した例です:

func TestComplexRequest(t *testing.T) {
    body := `{"key": "value"}`
    req := httptest.NewRequest("PUT", "/api/resource", strings.NewReader(body))
    req.Header.Set("Authorization", "Bearer token123")
    req.Header.Set("Content-Type", "application/json")

    if req.Method != "PUT" {
        t.Errorf("expected method PUT, got %s", req.Method)
    }
    if req.Header.Get("Authorization") != "Bearer token123" {
        t.Errorf("expected Authorization header, got %s", req.Header.Get("Authorization"))
    }
}

モックリクエストの応用


モックリクエストは、以下のようなテストシナリオで特に有用です:

  • 認証ヘッダーの検証
  • 複数のクエリパラメータを含むリクエストの処理
  • JSONやXMLを含むリクエストボディの解析

次章では、ハンドラテストの基本的な実践例を紹介します。

ハンドラテストの実践例:基本編


HTTPハンドラをテストする際の基本的な流れを具体例とともに解説します。ここでは、httptest.NewRecorderhttptest.NewRequestを使用して、シンプルなハンドラの動作を確認する方法を説明します。

基本的なHTTPハンドラのテスト


以下の例では、シンプルな「Hello, World!」を返すHTTPハンドラをテストします。

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func HelloWorldHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Hello, World!"))
}

func TestHelloWorldHandler(t *testing.T) {
    // モックリクエストとレスポンスレコーダを作成
    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()

    // ハンドラを呼び出し
    HelloWorldHandler(recorder, req)

    // ステータスコードの検証
    if recorder.Code != http.StatusOK {
        t.Errorf("expected status code %d, got %d", http.StatusOK, recorder.Code)
    }

    // レスポンスボディの検証
    if recorder.Body.String() != "Hello, World!" {
        t.Errorf("expected body 'Hello, World!', got '%s'", recorder.Body.String())
    }
}

ハンドラのパスパラメータを含むテスト


次の例は、リクエストのパスパラメータを処理するハンドラのテストです:

func GreetHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        http.Error(w, "Name is required", http.StatusBadRequest)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Hello, " + name + "!"))
}

func TestGreetHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/?name=John", nil)
    recorder := httptest.NewRecorder()

    GreetHandler(recorder, req)

    if recorder.Code != http.StatusOK {
        t.Errorf("expected status code %d, got %d", http.StatusOK, recorder.Code)
    }

    if recorder.Body.String() != "Hello, John!" {
        t.Errorf("expected body 'Hello, John!', got '%s'", recorder.Body.String())
    }
}

ハンドラでエラーを返す場合のテスト


エラーケースの処理を確認することも重要です。以下は、必要なパラメータが欠けている場合の動作をテストします:

func TestGreetHandlerError(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()

    GreetHandler(recorder, req)

    if recorder.Code != http.StatusBadRequest {
        t.Errorf("expected status code %d, got %d", http.StatusBadRequest, recorder.Code)
    }

    if recorder.Body.String() != "Name is required\n" {
        t.Errorf("expected body 'Name is required', got '%s'", recorder.Body.String())
    }
}

リクエストヘッダーの処理を含むハンドラ


次の例は、特定のヘッダーを必要とするハンドラのテストです:

func HeaderHandler(w http.ResponseWriter, r *http.Request) {
    if r.Header.Get("Authorization") == "" {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Authorized"))
}

func TestHeaderHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    req.Header.Set("Authorization", "Bearer token123")
    recorder := httptest.NewRecorder()

    HeaderHandler(recorder, req)

    if recorder.Code != http.StatusOK {
        t.Errorf("expected status code %d, got %d", http.StatusOK, recorder.Code)
    }

    if recorder.Body.String() != "Authorized" {
        t.Errorf("expected body 'Authorized', got '%s'", recorder.Body.String())
    }
}

まとめ


基本的なハンドラのテストでは、以下の点を確認します:

  • ステータスコードが正しいか
  • レスポンスボディが期待通りか
  • パスパラメータやリクエストヘッダーが正しく処理されているか

次章では、さらに複雑なロジックを持つハンドラの応用的なテスト方法を解説します。

ハンドラテストの実践例:応用編


応用的なハンドラテストでは、複雑なロジックや外部依存を含むHTTPハンドラの動作を検証します。この章では、ミドルウェアや外部サービスとの連携、条件分岐が多いハンドラをテストする方法を解説します。

コンテキスト情報を利用するハンドラのテスト


ハンドラがリクエストコンテキストから情報を取得する場合、その動作を検証する方法です。

package main

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"
)

func ContextHandler(w http.ResponseWriter, r *http.Request) {
    userID := r.Context().Value("userID")
    if userID == nil {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("User ID: " + userID.(string)))
}

func TestContextHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    ctx := context.WithValue(req.Context(), "userID", "12345")
    req = req.WithContext(ctx)

    recorder := httptest.NewRecorder()
    ContextHandler(recorder, req)

    if recorder.Code != http.StatusOK {
        t.Errorf("expected status code %d, got %d", http.StatusOK, recorder.Code)
    }

    if recorder.Body.String() != "User ID: 12345" {
        t.Errorf("expected body 'User ID: 12345', got '%s'", recorder.Body.String())
    }
}

外部サービスとの連携を模倣する


外部APIやデータベースと連携するハンドラのテストでは、モックオブジェクトを使用して依存性を取り除きます。

type MockService struct{}

func (m *MockService) GetData() string {
    return "Mock Data"
}

func DataHandler(service interface{ GetData() string }) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        data := service.GetData()
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(data))
    }
}

func TestDataHandler(t *testing.T) {
    mockService := &MockService{}
    handler := DataHandler(mockService)

    req := httptest.NewRequest("GET", "/data", nil)
    recorder := httptest.NewRecorder()

    handler.ServeHTTP(recorder, req)

    if recorder.Code != http.StatusOK {
        t.Errorf("expected status code %d, got %d", http.StatusOK, recorder.Code)
    }

    if recorder.Body.String() != "Mock Data" {
        t.Errorf("expected body 'Mock Data', got '%s'", recorder.Body.String())
    }
}

ミドルウェアを含むハンドラのテスト


ミドルウェアを挟んだ場合の動作を確認する方法です。以下は、認証ミドルウェアを含むテストの例です:

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func ProtectedHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Welcome, authorized user!"))
}

func TestAuthMiddleware(t *testing.T) {
    handler := AuthMiddleware(http.HandlerFunc(ProtectedHandler))

    // テスト1: 認証なし
    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()
    handler.ServeHTTP(recorder, req)
    if recorder.Code != http.StatusUnauthorized {
        t.Errorf("expected status code %d, got %d", http.StatusUnauthorized, recorder.Code)
    }

    // テスト2: 認証あり
    req = httptest.NewRequest("GET", "/", nil)
    req.Header.Set("Authorization", "Bearer valid_token")
    recorder = httptest.NewRecorder()
    handler.ServeHTTP(recorder, req)
    if recorder.Code != http.StatusOK {
        t.Errorf("expected status code %d, got %d", http.StatusOK, recorder.Code)
    }

    if recorder.Body.String() != "Welcome, authorized user!" {
        t.Errorf("expected body 'Welcome, authorized user!', got '%s'", recorder.Body.String())
    }
}

非同期処理を含むハンドラのテスト


非同期でデータを処理するハンドラをテストする場合も、httptestを活用できます。以下は簡単な例です:

func AsyncHandler(w http.ResponseWriter, r *http.Request) {
    go func() {
        // 非同期処理
        // ログ記録や通知送信など
    }()
    w.WriteHeader(http.StatusAccepted)
    w.Write([]byte("Processing started"))
}

func TestAsyncHandler(t *testing.T) {
    req := httptest.NewRequest("POST", "/", nil)
    recorder := httptest.NewRecorder()

    AsyncHandler(recorder, req)

    if recorder.Code != http.StatusAccepted {
        t.Errorf("expected status code %d, got %d", http.StatusAccepted, recorder.Code)
    }

    if recorder.Body.String() != "Processing started" {
        t.Errorf("expected body 'Processing started', got '%s'", recorder.Body.String())
    }
}

まとめ


応用的なハンドラのテストでは、外部依存や複雑なロジックをモックすることで、正確で効率的なテストが可能になります。次章では、効果的なテストを書くためのベストプラクティスを紹介します。

テストのベストプラクティス


HTTPハンドラのテストを効率的かつ正確に行うためには、いくつかのベストプラクティスを守ることが重要です。この章では、テストコードの品質を向上させるための具体的なポイントを解説します。

1. モジュール化されたテストコード


テストコードは分かりやすく、再利用可能にすることが重要です。テストケースごとに明確な責任を持たせ、適切にモジュール化します。

func executeRequest(handler http.Handler, method, path string, body string) *httptest.ResponseRecorder {
    req := httptest.NewRequest(method, path, strings.NewReader(body))
    recorder := httptest.NewRecorder()
    handler.ServeHTTP(recorder, req)
    return recorder
}

func TestReusableFunction(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Reusable test!"))
    })
    recorder := executeRequest(handler, "GET", "/", "")

    if recorder.Code != http.StatusOK {
        t.Errorf("expected status code %d, got %d", http.StatusOK, recorder.Code)
    }
}

2. 明確な期待値を設定


テストの期待値(ステータスコード、レスポンスボディなど)は具体的であるべきです。また、エラーメッセージはテスト失敗時に原因が分かるように記述します。

func TestSpecificExpectation(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("Success!"))
    })

    recorder := executeRequest(handler, "GET", "/", "")

    if recorder.Body.String() != "Success!" {
        t.Errorf("unexpected body: got %s, want %s", recorder.Body.String(), "Success!")
    }
}

3. 異常系のテストを重視


異常系(エラーや境界ケース)を網羅的にテストすることで、予期せぬ挙動を防ぐことができます。

func TestErrorHandling(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Error(w, "Something went wrong", http.StatusInternalServerError)
    })

    recorder := executeRequest(handler, "GET", "/", "")

    if recorder.Code != http.StatusInternalServerError {
        t.Errorf("expected status code %d, got %d", http.StatusInternalServerError, recorder.Code)
    }

    if recorder.Body.String() != "Something went wrong\n" {
        t.Errorf("unexpected body: got %s, want %s", recorder.Body.String(), "Something went wrong\n")
    }
}

4. テストデータの準備を工夫する


テストデータを固定化またはモック化して、一貫性のある結果が得られるようにします。たとえば、ファイルやデータベースの依存を取り除くことが有効です。

type MockDatabase struct{}

func (m *MockDatabase) GetUser() string {
    return "John Doe"
}

func TestWithMockDatabase(t *testing.T) {
    db := &MockDatabase{}
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := db.GetUser()
        w.Write([]byte("User: " + user))
    })

    recorder := executeRequest(handler, "GET", "/", "")

    if recorder.Body.String() != "User: John Doe" {
        t.Errorf("unexpected body: got %s, want %s", recorder.Body.String(), "User: John Doe")
    }
}

5. パラメータ化されたテスト


同じロジックを異なる条件でテストする場合は、パラメータ化されたテストを利用します。

func TestParameterized(t *testing.T) {
    testCases := []struct {
        method         string
        path           string
        expectedStatus int
        expectedBody   string
    }{
        {"GET", "/", http.StatusOK, "Hello, World!"},
        {"POST", "/", http.StatusMethodNotAllowed, "Method Not Allowed"},
    }

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != "GET" {
            http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
            return
        }
        w.Write([]byte("Hello, World!"))
    })

    for _, tc := range testCases {
        recorder := executeRequest(handler, tc.method, tc.path, "")
        if recorder.Code != tc.expectedStatus {
            t.Errorf("unexpected status code: got %d, want %d", recorder.Code, tc.expectedStatus)
        }
        if recorder.Body.String() != tc.expectedBody {
            t.Errorf("unexpected body: got %s, want %s", recorder.Body.String(), tc.expectedBody)
        }
    }
}

6. 継続的インテグレーション(CI)での自動実行


テストは手動で実行するだけでなく、CIツールを使用して自動的に実行する仕組みを整えると、品質を継続的に保証できます。

まとめ


テストコードの設計と実行には、再現性、モジュール化、異常系のテストの網羅性を意識することが重要です。次章では、httptestと他のライブラリの組み合わせについて解説します。

他のテストライブラリとの併用


httptestは単体で強力なテストツールですが、他のライブラリと組み合わせることで、さらに効率的で包括的なテストが可能になります。この章では、Goのテストライブラリを活用したテスト手法を紹介します。

1. `testify`との併用


Testifyは、Goの人気ライブラリで、テストを簡素化するアサーション機能を提供します。httptestと併用することで、コードの冗長性を減らせます。

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestWithTestify(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("Hello, Testify!"))
    })

    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()
    handler.ServeHTTP(recorder, req)

    assert.Equal(t, http.StatusOK, recorder.Code, "Status code should be 200")
    assert.Equal(t, "Hello, Testify!", recorder.Body.String(), "Response body mismatch")
}

2. `gomock`を使用したモック生成


GoMockは、モックを生成するためのライブラリです。外部依存(例:データベースや外部API)をモックし、httptestと併用することで、依存性を制御したテストが可能です。

// モックインターフェースを事前に生成する必要があります
import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/golang/mock/gomock"
)

type MockService struct{}

func (m *MockService) GetData() string {
    return "Mock Data"
}

func TestWithGoMock(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockService := NewMockMyService(ctrl)
    mockService.EXPECT().GetData().Return("Mocked Data")

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        data := mockService.GetData()
        w.Write([]byte(data))
    })

    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()
    handler.ServeHTTP(recorder, req)

    if recorder.Body.String() != "Mocked Data" {
        t.Errorf("unexpected body: got %s, want %s", recorder.Body.String(), "Mocked Data")
    }
}

3. `httpexpect`を使用したE2Eテスト


httpexpectは、HTTPハンドラのエンドツーエンドテストを簡単に行えるライブラリです。httptestのモックサーバーと組み合わせることで、統合テストが効率的になります。

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gavv/httpexpect/v2"
)

func TestWithHttpExpect(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/hello" {
            w.Write([]byte("Hello, World!"))
            return
        }
        http.NotFound(w, r)
    })

    server := httptest.NewServer(handler)
    defer server.Close()

    e := httpexpect.New(t, server.URL)

    e.GET("/hello").
        Expect().
        Status(http.StatusOK).
        Body().Equal("Hello, World!")

    e.GET("/unknown").
        Expect().
        Status(http.StatusNotFound)
}

4. `go-cmp`による構造体比較


go-cmpは、構造体の詳細な比較を可能にするライブラリです。レスポンスの内容が複雑な場合に特に有用です。

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/google/go-cmp/cmp"
)

func TestWithGoCmp(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{"message": "Hello, JSON!"})
    })

    req := httptest.NewRequest("GET", "/", nil)
    recorder := httptest.NewRecorder()
    handler.ServeHTTP(recorder, req)

    var got map[string]string
    json.Unmarshal(recorder.Body.Bytes(), &got)

    want := map[string]string{"message": "Hello, JSON!"}
    if diff := cmp.Diff(want, got); diff != "" {
        t.Errorf("unexpected response (-want +got):\n%s", diff)
    }
}

5. `ginkgo`と`gomega`によるBDDスタイルのテスト


GinkgoGomegaは、BDD(振る舞い駆動開発)スタイルのテストを可能にします。

import (
    "net/http"
    "net/http/httptest"
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
)

var _ = Describe("HTTP Handler", func() {
    It("should return Hello, Ginkgo!", func() {
        handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("Hello, Ginkgo!"))
        })

        req := httptest.NewRequest("GET", "/", nil)
        recorder := httptest.NewRecorder()
        handler.ServeHTTP(recorder, req)

        Expect(recorder.Code).To(Equal(http.StatusOK))
        Expect(recorder.Body.String()).To(Equal("Hello, Ginkgo!"))
    })
})

まとめ


httptestを他のライブラリと併用することで、テストの可読性や効率が大幅に向上します。これらのライブラリを適切に活用し、プロジェクトに最適なテスト戦略を構築しましょう。次章では、この記事の内容をまとめます。

まとめ


本記事では、Go言語のhttptestパッケージを用いたHTTPハンドラテストの方法を、基礎から応用まで解説しました。httptest.NewRecorderhttptest.NewRequestを活用することで、HTTPハンドラのレスポンスやエラーハンドリングを簡単に検証できることを学びました。さらに、testifygomockなどのライブラリを併用することで、モックの生成やテストの効率化を実現できることも示しました。

HTTPハンドラのテストは、アプリケーションの安定性と品質を保証するために不可欠です。今回学んだテクニックを活用し、実践的で効果的なテストスイートを構築していきましょう。

コメント

コメントする

目次
  1. Go言語におけるHTTPハンドラの基本概念
    1. HTTPハンドラの基本構造
    2. HTTPハンドラの役割
    3. HTTPハンドラのテストの重要性
  2. `httptest`パッケージの概要
    1. `httptest`の主な機能
    2. なぜ`httptest`を使用するのか
    3. 基本的な使い方
  3. `httptest.NewRecorder`の活用法
    1. モックレコーダの役割
    2. `httptest.NewRecorder`の基本的な使い方
    3. レスポンスヘッダーの検証
    4. レスポンスボディの検証
    5. 複雑なレスポンスの検証
  4. HTTPリクエストのモック作成方法
    1. モックリクエストの基本
    2. URLパラメータのモック
    3. リクエストヘッダーの設定
    4. リクエストボディのモック
    5. 複雑なリクエストのモック
    6. モックリクエストの応用
  5. ハンドラテストの実践例:基本編
    1. 基本的なHTTPハンドラのテスト
    2. ハンドラのパスパラメータを含むテスト
    3. ハンドラでエラーを返す場合のテスト
    4. リクエストヘッダーの処理を含むハンドラ
    5. まとめ
  6. ハンドラテストの実践例:応用編
    1. コンテキスト情報を利用するハンドラのテスト
    2. 外部サービスとの連携を模倣する
    3. ミドルウェアを含むハンドラのテスト
    4. 非同期処理を含むハンドラのテスト
    5. まとめ
  7. テストのベストプラクティス
    1. 1. モジュール化されたテストコード
    2. 2. 明確な期待値を設定
    3. 3. 異常系のテストを重視
    4. 4. テストデータの準備を工夫する
    5. 5. パラメータ化されたテスト
    6. 6. 継続的インテグレーション(CI)での自動実行
    7. まとめ
  8. 他のテストライブラリとの併用
    1. 1. `testify`との併用
    2. 2. `gomock`を使用したモック生成
    3. 3. `httpexpect`を使用したE2Eテスト
    4. 4. `go-cmp`による構造体比較
    5. 5. `ginkgo`と`gomega`によるBDDスタイルのテスト
    6. まとめ
  9. まとめ