Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14056] ファイルの概要

このコミットは、Go言語の標準ライブラリ net/http/httptest パッケージ内の ResponseRecorder の挙動を改善し、実際のHTTPサーバーの http.ResponseWriter の動作により近づけることを目的としています。具体的には、WriteHeader メソッドの複数回呼び出しや、Write メソッドが呼び出された際のステータスコードの自動設定など、実際の http.ResponseWriter が持つ特性を ResponseRecorder に反映させています。また、これらの変更を検証するためのテストコードも新たに追加されています。

コミット

commit 13576e3b6587dcde0f5df3d04449ca16c88dcda2
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Sun Oct 7 09:48:14 2012 -0700

    net/http/httptest: mimic the normal HTTP server's ResponseWriter more closely
    
    Also adds tests, which didn't exist before.
    
    Fixes #4188
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6613062

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/13576e3b6587dcde0f5df3d04449ca16c88dcda2

元コミット内容

このコミットの元の内容は以下の通りです。

  • net/http/httptest: 通常のHTTPサーバーの ResponseWriter の挙動をより忠実に模倣する。
  • 以前は存在しなかったテストも追加する。
  • Issue #4188 を修正する。

変更の背景

Go言語の net/http/httptest パッケージは、HTTPハンドラやサーバーのテストを容易にするために設計されています。ResponseRecorder は、http.ResponseWriter インターフェースをインメモリで実装したもので、HTTPハンドラが生成するレスポンス(ステータスコード、ヘッダー、ボディ)をキャプチャし、テスト内で検証できるようにします。

しかし、このコミット以前の ResponseRecorder は、実際の http.ResponseWriter の挙動を完全に模倣しているわけではありませんでした。特に、以下の点が問題となっていました。

  1. WriteHeader の複数回呼び出し: 実際の http.ResponseWriter では、WriteHeader は一度しか効果を持ちません。二度目以降の呼び出しは無視されます。しかし、以前の ResponseRecorder は、複数回 WriteHeader が呼び出された場合に、最後の呼び出しでステータスコードが上書きされてしまう可能性がありました。
  2. Write 呼び出し時のステータスコード: 実際のHTTPサーバーでは、Write メソッドが呼び出される前に WriteHeader が明示的に呼び出されていない場合、自動的に 200 OK のステータスコードが設定されます。以前の ResponseRecorder はこの挙動を模倣していませんでした。
  3. Flush 呼び出し時のステータスコード: http.Flusher インターフェースを実装している ResponseWriterFlush メソッドが呼び出された際も、同様に WriteHeader が呼び出されていない場合は 200 OK が自動的に設定されるべきです。
  4. テストの不足: ResponseRecorder のこれらの挙動を検証するためのテストが不足していました。

これらの不一致は、httptest を使用したテストが実際のサーバー環境での動作と異なる結果をもたらす可能性があり、テストの信頼性を損なう原因となっていました。Issue #4188 は、これらの問題点を指摘し、ResponseRecorder の挙動をより正確にすることの必要性を示唆していたと考えられます。

前提知識の解説

Go言語の net/http パッケージ

Go言語の net/http パッケージは、HTTPクライアントとサーバーの実装を提供します。ウェブアプリケーションを構築する上で中心的な役割を担います。

  • http.Handler インターフェース: HTTPリクエストを処理するためのインターフェースです。ServeHTTP(w http.ResponseWriter, r *http.Request) メソッドを一つ持ちます。
  • http.ResponseWriter インターフェース: HTTPレスポンスを構築するためのインターフェースです。以下の主要なメソッドを持ちます。
    • Header() Header: レスポンスヘッダーを返します。
    • Write([]byte) (int, error): レスポンスボディにデータを書き込みます。このメソッドが呼び出される前に WriteHeader が呼び出されていない場合、自動的に 200 OK が設定されます。
    • WriteHeader(statusCode int): HTTPステータスコードを設定します。このメソッドは一度しか効果を持ちません。
  • http.Flusher インターフェース: http.ResponseWriterFlush() メソッドをサポートしている場合に実装されるオプションのインターフェースです。Flush() は、バッファリングされたデータをクライアントに送信します。

Go言語の net/http/httptest パッケージ

net/http/httptest パッケージは、net/http パッケージで構築されたHTTPハンドラやサーバーのテストを支援するためのユーティリティを提供します。

  • httptest.ResponseRecorder: http.ResponseWriter インターフェースを実装した構造体で、HTTPハンドラが生成するレスポンス(ステータスコード、ヘッダー、ボディ)をメモリ上に記録します。これにより、実際のネットワーク通信なしにHTTPハンドラの出力を検証できます。
    • Code: 記録されたHTTPステータスコード。
    • HeaderMap: 記録されたHTTPヘッダー。
    • Body: 記録されたレスポンスボディ。

Issue #4188

コミットメッセージに Fixes #4188 と記載されていますが、Web検索ではこの特定のIssueの詳細な内容は直接見つかりませんでした。しかし、コミットの変更内容から推測すると、ResponseRecorder が実際の http.ResponseWriter の挙動と異なる点があり、それがテストの信頼性や正確性に影響を与えていた問題であると考えられます。特に、WriteHeader の複数回呼び出しの処理や、Write および Flush 呼び出し時のデフォルトステータスコードの設定に関する不一致が主な論点だったと推測されます。

技術的詳細

このコミットは、httptest.ResponseRecorder の内部実装に wroteHeader という新しいフィールドを導入し、WriteHeaderWriteFlush メソッドのロジックを変更することで、実際の http.ResponseWriter の挙動をより正確に模倣しています。

  1. ResponseRecorder 構造体の変更:

    • wroteHeader bool フィールドが追加されました。これは、WriteHeader が一度でも呼び出されたかどうかを追跡するために使用されます。
    • NewRecorder() 関数で Code フィールドの初期値が 200 に設定されました。これは、明示的にステータスコードが設定されない場合のデフォルト値となります。
  2. Header() メソッドの変更:

    • HeaderMapnil の場合に、新しい http.Header マップを作成して rw.HeaderMap に割り当てるようになりました。これにより、Header() メソッドが常に有効なヘッダーマップを返すことが保証されます。
  3. Write() メソッドの変更:

    • !rw.wroteHeader の場合、つまりまだヘッダーが書き込まれていない場合に、rw.WriteHeader(200) を呼び出すようになりました。これにより、実際の http.ResponseWriter と同様に、Write が最初に呼び出された際に自動的に 200 OK のステータスコードが設定される挙動が模倣されます。
    • 以前は rw.Code == 0 の場合に rw.Code = http.StatusOK を設定していましたが、このロジックは削除されました。これは wroteHeader の導入により不要になったためです。
  4. WriteHeader() メソッドの変更:

    • !rw.wroteHeader の場合のみ、rw.Code = code を実行するように変更されました。これにより、WriteHeader が複数回呼び出されても、最初の呼び出しで設定されたステータスコードのみが有効となり、以降の呼び出しは無視されるという実際の http.ResponseWriter の挙動が再現されます。
    • rw.wroteHeader = true が設定され、ヘッダーが書き込まれたことを記録します。
  5. Flush() メソッドの変更:

    • !rw.wroteHeader の場合、つまりまだヘッダーが書き込まれていない場合に、rw.WriteHeader(200) を呼び出すようになりました。これにより、Flush が最初に呼び出された際に自動的に 200 OK のステータスコードが設定される挙動が模倣されます。

これらの変更により、httptest.ResponseRecorder は、HTTPハンドラのテストにおいて、より現実のHTTPサーバーに近い環境を提供できるようになり、テストの正確性と信頼性が向上しました。

コアとなるコードの変更箇所

src/pkg/net/http/httptest/recorder.go

--- a/src/pkg/net/http/httptest/recorder.go
+++ b/src/pkg/net/http/httptest/recorder.go
@@ -17,6 +17,8 @@ type ResponseRecorder struct {
 	HeaderMap http.Header   // the HTTP response headers
 	Body      *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
 	Flushed   bool
+
+	wroteHeader bool
 }
 
 // NewRecorder returns an initialized ResponseRecorder.
@@ -24,6 +26,7 @@ func NewRecorder() *ResponseRecorder {
 	return &ResponseRecorder{
 		HeaderMap: make(http.Header),\n 	\tBody:      new(bytes.Buffer),\n+\t\tCode:      200,\n 	}\n }\n \n@@ -33,26 +36,37 @@ const DefaultRemoteAddr = "1.2.3.4"\n \n // Header returns the response headers.\n func (rw *ResponseRecorder) Header() http.Header {\n-\treturn rw.HeaderMap
+\tm := rw.HeaderMap\n+\tif m == nil {\n+\t\tm = make(http.Header)\n+\t\trw.HeaderMap = m\n+\t}\n+\treturn m
 }\n \n // Write always succeeds and writes to rw.Body, if not nil.\n func (rw *ResponseRecorder) Write(buf []byte) (int, error) {\n+\tif !rw.wroteHeader {\n+\t\trw.WriteHeader(200)\n+\t}\
 \tif rw.Body != nil {\n \t\trw.Body.Write(buf)\n \t}\
-\tif rw.Code == 0 {\n-\t\trw.Code = http.StatusOK\n-\t}\
 \treturn len(buf), nil
 }\n \n // WriteHeader sets rw.Code.\n func (rw *ResponseRecorder) WriteHeader(code int) {\n-\trw.Code = code
+\tif !rw.wroteHeader {\n+\t\trw.Code = code\n+\t}\n+\trw.wroteHeader = true
 }\n \n // Flush sets rw.Flushed to true.\n func (rw *ResponseRecorder) Flush() {\n+\tif !rw.wroteHeader {\n+\t\trw.WriteHeader(200)\n+\t}\
 \trw.Flushed = true
 }\n```

### `src/pkg/net/http/httptest/recorder_test.go` (新規ファイル)

```go
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package httptest

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

func TestRecorder(t *testing.T) {
	type checkFunc func(*ResponseRecorder) error
	check := func(fns ...checkFunc) []checkFunc { return fns }

	hasStatus := func(wantCode int) checkFunc {
		return func(rec *ResponseRecorder) error {
			if rec.Code != wantCode {
				return fmt.Errorf("Status = %d; want %d", rec.Code, wantCode)
			}
			return nil
		}
	}
	hasContents := func(want string) checkFunc {
		return func(rec *ResponseRecorder) error {
			if rec.Body.String() != want {
				return fmt.Errorf("wrote = %q; want %q", rec.Body.String(), want)
			}
			return nil
		}
	}
	hasFlush := func(want bool) checkFunc {
		return func(rec *ResponseRecorder) error {
			if rec.Flushed != want {
				return fmt.Errorf("Flushed = %v; want %v", rec.Flushed, want)
			}
			return nil
		}
	}

	tests := []struct {
		name   string
		h      func(w http.ResponseWriter, r *http.Request)
		checks []checkFunc
	}{
		{
			"200 default",
			func(w http.ResponseWriter, r *http.Request) {},
			check(hasStatus(200), hasContents("")),
		},
		{
			"first code only",
			func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(201)
				w.WriteHeader(202)
				w.Write([]byte("hi"))
			},
			check(hasStatus(201), hasContents("hi")),
		},
		{
			"write sends 200",
			func(w http.ResponseWriter, r *http.Request) {
				w.Write([]byte("hi first"))
				w.WriteHeader(201)
				w.WriteHeader(202)
			},
			check(hasStatus(200), hasContents("hi first"), hasFlush(false)),
		},
		{
			"flush",
			func(w http.ResponseWriter, r *http.Request) {
				w.(http.Flusher).Flush() // also sends a 200
				w.WriteHeader(201)
			},
			check(hasStatus(200), hasFlush(true)),
		},
	}
	r, _ := http.NewRequest("GET", "http://foo.com/", nil)
	for _, tt := range tests {
		h := http.HandlerFunc(tt.h)
		rec := NewRecorder()
		h.ServeHTTP(rec, r)
		for _, check := range tt.checks {
			if err := check(rec); err != nil {
				t.Errorf("%s: %v", tt.name, err)
			}
		}
	}
}

コアとなるコードの解説

src/pkg/net/http/httptest/recorder.go の変更点

  • ResponseRecorder 構造体:
    • wroteHeader bool フィールドが追加されました。これは、WriteHeader が一度でも呼び出されたかどうかを追跡するためのフラグです。このフラグにより、WriteHeader の複数回呼び出しに対する挙動を制御できるようになります。
  • NewRecorder() 関数:
    • Code: 200, が追加され、新しく作成される ResponseRecorder のデフォルトのステータスコードが 200 OK に初期化されるようになりました。これにより、明示的にステータスコードが設定されない場合の挙動がより明確になります。
  • Header() メソッド:
    • rw.HeaderMapnil の場合に make(http.Header) で新しいマップを作成し、rw.HeaderMap に割り当てるロジックが追加されました。これにより、Header() メソッドが呼び出された際に常に有効なヘッダーマップが返されることが保証され、nil ポインタ参照によるパニックを防ぎます。
  • Write() メソッド:
    • if !rw.wroteHeader { rw.WriteHeader(200) } の行が追加されました。これは、Write メソッドが呼び出される前に WriteHeader が明示的に呼び出されていない場合(つまり wroteHeaderfalse の場合)、自動的に 200 OK のステータスコードを設定するという、実際の http.ResponseWriter の重要な挙動を模倣しています。
    • 以前存在した if rw.Code == 0 { rw.Code = http.StatusOK } のロジックは削除されました。これは wroteHeader フラグと新しい WriteHeader のロジックにより不要になったためです。
  • WriteHeader() メソッド:
    • if !rw.wroteHeader { rw.Code = code } の条件が追加されました。これにより、WriteHeader が複数回呼び出されても、最初の呼び出しで設定されたステータスコードのみが rw.Code に反映され、以降の呼び出しは無視されるようになります。これは、実際の http.ResponseWriter の挙動と一致します。
    • rw.wroteHeader = true が追加され、一度 WriteHeader が呼び出されたことを記録します。
  • Flush() メソッド:
    • if !rw.wroteHeader { rw.WriteHeader(200) } の行が追加されました。これは Write() メソッドと同様に、Flush が呼び出される前に WriteHeader が明示的に呼び出されていない場合、自動的に 200 OK のステータスコードを設定するという挙動を模倣しています。

src/pkg/net/http/httptest/recorder_test.go の新規追加

このファイルは、ResponseRecorder の新しい挙動を検証するための包括的なテストスイートを提供します。

  • checkFunc: ResponseRecorder の状態を検証するための関数型を定義しています。
  • hasStatus, hasContents, hasFlush ヘルパー関数: それぞれ、ステータスコード、ボディの内容、Flushed フラグの状態を検証するための checkFunc を生成するヘルパー関数です。これにより、テストコードの可読性と再利用性が向上しています。
  • tests スライス: 複数のテストケースを定義しています。各テストケースは以下の要素を持ちます。
    • name: テストケースの名前。
    • h: テスト対象の http.HandlerFunc。この関数内で http.ResponseWriter (実際には ResponseRecorder) のメソッドが呼び出され、その挙動が検証されます。
    • checks: そのテストケースで実行される checkFunc のスライス。
  • テストケースの例:
    • "200 default": WriteHeaderWrite が何も呼び出されない場合に、デフォルトでステータスコードが 200 になることを検証します。
    • "first code only": WriteHeader が複数回呼び出されても、最初の呼び出しで設定されたステータスコード (201) のみが有効になり、その後の呼び出し (202) は無視されることを検証します。
    • "write sends 200": Write が最初に呼び出された際に、明示的な WriteHeader がなくても自動的にステータスコードが 200 に設定されることを検証します。
    • "flush": Flush が呼び出された際に、明示的な WriteHeader がなくても自動的にステータスコードが 200 に設定されることを検証します。
  • テスト実行ロジック:
    • 各テストケースに対して httptest.NewRecorder() で新しい ResponseRecorder を作成し、テスト対象のハンドラ (tt.h) を ServeHTTP メソッドで実行します。
    • その後、定義された checkFunc をループで実行し、ResponseRecorder の状態が期待通りであるかを検証します。

これらのテストは、ResponseRecorder が実際の http.ResponseWriter の挙動を正確に模倣していることを保証するための重要な役割を果たします。

関連リンク

参考にした情報源リンク

  • Web検索結果 (httptest.ResponseRecorder の一般的な説明):
    • golang.cafe: httptest.ResponseRecorder の概要
    • github.com (Go言語のソースコード): httptest.ResponseRecorder の実装
    • go.dev (Go言語の公式ドキュメント): httptest.ResponseRecorder の使用方法
    • dev.to, speedscale.com, itnext.io: httptest.ResponseRecorder を用いたテストに関する記事
  • GitHub (Go言語リポジトリ): コミット 13576e3b6587dcde0f5df3d04449ca16c88dcda2
  • Go言語のIssueトラッカー (Issue #60229): http.ResponseController のサポートに関する最近の強化(直接的な関連はないが、httptest.ResponseRecorder の進化を示す情報として参照)