[インデックス 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 の挙動を完全に模倣しているわけではありませんでした。特に、以下の点が問題となっていました。
WriteHeaderの複数回呼び出し: 実際のhttp.ResponseWriterでは、WriteHeaderは一度しか効果を持ちません。二度目以降の呼び出しは無視されます。しかし、以前のResponseRecorderは、複数回WriteHeaderが呼び出された場合に、最後の呼び出しでステータスコードが上書きされてしまう可能性がありました。Write呼び出し時のステータスコード: 実際のHTTPサーバーでは、Writeメソッドが呼び出される前にWriteHeaderが明示的に呼び出されていない場合、自動的に200 OKのステータスコードが設定されます。以前のResponseRecorderはこの挙動を模倣していませんでした。Flush呼び出し時のステータスコード:http.Flusherインターフェースを実装しているResponseWriterのFlushメソッドが呼び出された際も、同様にWriteHeaderが呼び出されていない場合は200 OKが自動的に設定されるべきです。- テストの不足:
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.ResponseWriterがFlush()メソッドをサポートしている場合に実装されるオプションのインターフェースです。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 という新しいフィールドを導入し、WriteHeader、Write、Flush メソッドのロジックを変更することで、実際の http.ResponseWriter の挙動をより正確に模倣しています。
-
ResponseRecorder構造体の変更:wroteHeader boolフィールドが追加されました。これは、WriteHeaderが一度でも呼び出されたかどうかを追跡するために使用されます。NewRecorder()関数でCodeフィールドの初期値が200に設定されました。これは、明示的にステータスコードが設定されない場合のデフォルト値となります。
-
Header()メソッドの変更:HeaderMapがnilの場合に、新しいhttp.Headerマップを作成してrw.HeaderMapに割り当てるようになりました。これにより、Header()メソッドが常に有効なヘッダーマップを返すことが保証されます。
-
Write()メソッドの変更:!rw.wroteHeaderの場合、つまりまだヘッダーが書き込まれていない場合に、rw.WriteHeader(200)を呼び出すようになりました。これにより、実際のhttp.ResponseWriterと同様に、Writeが最初に呼び出された際に自動的に200 OKのステータスコードが設定される挙動が模倣されます。- 以前は
rw.Code == 0の場合にrw.Code = http.StatusOKを設定していましたが、このロジックは削除されました。これはwroteHeaderの導入により不要になったためです。
-
WriteHeader()メソッドの変更:!rw.wroteHeaderの場合のみ、rw.Code = codeを実行するように変更されました。これにより、WriteHeaderが複数回呼び出されても、最初の呼び出しで設定されたステータスコードのみが有効となり、以降の呼び出しは無視されるという実際のhttp.ResponseWriterの挙動が再現されます。rw.wroteHeader = trueが設定され、ヘッダーが書き込まれたことを記録します。
-
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.HeaderMapがnilの場合にmake(http.Header)で新しいマップを作成し、rw.HeaderMapに割り当てるロジックが追加されました。これにより、Header()メソッドが呼び出された際に常に有効なヘッダーマップが返されることが保証され、nilポインタ参照によるパニックを防ぎます。
Write()メソッド:if !rw.wroteHeader { rw.WriteHeader(200) }の行が追加されました。これは、Writeメソッドが呼び出される前にWriteHeaderが明示的に呼び出されていない場合(つまりwroteHeaderがfalseの場合)、自動的に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":
WriteHeaderやWriteが何も呼び出されない場合に、デフォルトでステータスコードが200になることを検証します。 - "first code only":
WriteHeaderが複数回呼び出されても、最初の呼び出しで設定されたステータスコード (201) のみが有効になり、その後の呼び出し (202) は無視されることを検証します。 - "write sends 200":
Writeが最初に呼び出された際に、明示的なWriteHeaderがなくても自動的にステータスコードが200に設定されることを検証します。 - "flush":
Flushが呼び出された際に、明示的なWriteHeaderがなくても自動的にステータスコードが200に設定されることを検証します。
- "200 default":
- テスト実行ロジック:
- 各テストケースに対して
httptest.NewRecorder()で新しいResponseRecorderを作成し、テスト対象のハンドラ (tt.h) をServeHTTPメソッドで実行します。 - その後、定義された
checkFuncをループで実行し、ResponseRecorderの状態が期待通りであるかを検証します。
- 各テストケースに対して
これらのテストは、ResponseRecorder が実際の http.ResponseWriter の挙動を正確に模倣していることを保証するための重要な役割を果たします。
関連リンク
- Go言語の
net/httpパッケージドキュメント: https://pkg.go.dev/net/http - Go言語の
net/http/httptestパッケージドキュメント: https://pkg.go.dev/net/http/httptest
参考にした情報源リンク
- 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を用いたテストに関する記事
- golang.cafe:
- GitHub (Go言語リポジトリ): コミット
13576e3b6587dcde0f5df3d04449ca16c88dcda2 - Go言語のIssueトラッカー (Issue #60229):
http.ResponseControllerのサポートに関する最近の強化(直接的な関連はないが、httptest.ResponseRecorderの進化を示す情報として参照)