[インデックス 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
の進化を示す情報として参照)