net/http/httptestにインメモリネットワークとtesting.TB連携を備えた新コンストラクタNewTestServerを追加する
net/http/httptest
概要
net/http/httptestパッケージに新しいコンストラクタ
NewTestServerが追加される。
NewTestServer(t testing.TB, handler http.Handler) *ServerはデフォルトでOSのネットワークスタックを
使わないインメモリネットワークを使用し、testing/synctest
パッケージと組み合わせて使えるHTTPテストサーバーを返す。testing.TBを受け取ることで、ハンドラが
パニックした場合の自動テスト失敗、およびテスト終了時のサーバー自動クリーンアップ(t.Cleanup)も
行われる。明示的にServer.StartまたはServer.StartTLSを呼べば、従来どおりループバックインター
フェース上の実TCPソケットを使う動作に切り替えられる。
導入経緯
提案者はhttptest.NewServerを多用しているが、OSのネットワークスタックを使うため
testing/synctestと組み合わせられないという課題を挙げ、net.Pipe()を手動で組み立てて
インメモリ接続を作る現状のワークアラウンドが複雑であることを指摘した(go.dev/play/p/AVXzqqwiJPn参照)。
当初の提案名はNewSynctestServer(handler http.Handler) *Serverだったが、レビューの過程で
「synctest固有の要素はない」という指摘を受けNewFakeNetServerに改名され、最終的には
「テストに適した推奨コンストラクタ」であることを示すNewTestServerに落ち着いた。長年未解決だった
net/http/httptest: optional faster test server #14200
とも実質的に同じ課題を扱っている。
議論タイムライン
- 2026-04-15: Proposal Reviewミーティングでactiveカラムに追加され、週次レビューの対象となった。
- 2026-05-06:
@aclementsが"likely accept"と判定。APIはNewTestServer(t testing.TB, handler http.Handler) *Serverに収束した。 - 2026-05-13: コンセンサスに変更がないことが確認され、正式に"accepted"となった。
議論のハイライト
- 命名の変遷:
NewSynctestServer→NewFakeNetServer→NewTestServer。synctest専用ではなく
一般的なテスト向けサーバーという位置づけが最終的に採用された。 testing.TB引数はスコープクリープ: 当初の提案にはなかったが、ハンドラパニックの即時テスト
失敗報告と自動クリーンアップという実用上のメリットが評価され採用された。net/http/httptestは
パッケージ名に"test"を含むため、testingパッケージへの依存追加は許容された。- インメモリ/ループバックの切り替え条件:
Server.Clientを最初に呼ぶとインメモリネットワーク、
Server.StartまたはServer.StartTLSを最初に呼ぶとループバックネットワークになるという遅延初期化
方式が採用された。 - ユーザー提供
Listenerは非サポート:NewTestServerで作成したサーバーに独自のnet.Listenerを
設定することは許可されず、将来の設計自由度を残すためパニックする仕様となった。 - 複数サーバーでのフェイクネット共有は対象外: リバースプロキシのテストなどで複数サーバーが
同一のフェイクネットワークを共有するケースは今回のスコープ外とされ、
proposal: testing/nettest: add in-memory networking implementation #77362
の進展を待つ方針となった。 - 既存コンストラクタは維持:
NewServer/NewTLSServer/NewUnstartedServerは後方互換のため残し、
ドキュメント上で新規コードにはNewTestServerを推奨する形にとどめた。
使用例
Before
package httptest_test
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestHandler(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
}))
defer server.Close()
resp, err := server.Client().Get(server.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
}
After
package httptest_test
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"testing/synctest"
)
func TestHandler(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
server := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
}))
// インメモリネットワークが使われるため、宛先ホスト名を問わずリクエストはserverに届く。
resp, err := server.Client().Get("http://example.com/")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// サーバーはt.Cleanupで自動的にCloseされるので、defer server.Close()は不要。
})
}
移行時の注意
NewTestServerが返すサーバーのServer.Listenerはインメモリネットワーク使用時には設定されない。
独自のnet.Listenerを差し込みたい場合はNewTestServerではパニックするため、従来どおり
NewUnstartedServerを使う必要がある。- インメモリネットワーク使用時、
Server.Clientが返すhttp.Clientは宛先ホスト名やアドレスに
関係なくすべてのリクエストをテストサーバーに送る。特定ホストのみに絞りたい既存のテストを
移行する場合は挙動差に注意する。 - 実ループバックネットワークを使い続けたい場合は
Server.StartまたはServer.StartTLSを明示的に
呼ぶ。この場合Server.Clientはexample.comまたはそのサブドメイン宛のリクエストのみを
サーバーへ転送する(既存のhttptest.NewServerとは異なりホスト名ベースの振り分けになる)。 handlerにnilを渡した場合、http.DefaultServeMuxは使われず、常に500応答を返すハンドラに
なる。
実装解説
go-src(golang/go master)のsrc/net/http/httptest/server.goを確認した。
NewTestServer
はConfig.Handlerにパニック処理用のtestServerHandlerをラップして設定し、
t.Cleanup(func() { s.Close() })でサーバーの自動シャットダウンを登録するだけで、
この時点ではネットワークの起動は行わない。testServerHandler.ServeHTTP
がハンドラのパニックをrecoverし、パニック値がhttp.ErrAbortHandler以外ならt.Errorfで
テストを失敗させたうえで、http.Server側の標準エラーへのログ出力を抑制するために
http.ErrAbortHandlerとして再パニックする。Server.Client
はs.t != nil(=NewTestServer経由で作られたサーバー)の場合のみ
s.startOnce.Do(s.startFakeNet)で遅延初期化を行う。NewServerなど従来のコンストラクタでは
tがnilのためこの分岐は通らない。startFakeNet
はinternal/nettestのnettest.ListenerをHTTP用・HTTPS用に1つずつ作成し、
http.Transport.DialContext/DialTLSContextを宛先に関わらず常にそのフェイクリスナーへ
接続する実装に差し替える。TLSクライアントにはInsecureSkipVerifyを設定し、
Server.URLは固定で"http://example.com"となる。Server.Start
/StartTLS
は共通処理startCommon
を経由し、useLoopback=trueかつs.t != nilの場合はs.Listenerが未設定であることを確認した
うえでnewLocalListener()が返す実TCPリスナーを設定する。すでにs.Listenerが設定されていると
ここでpanic("Server.Listener is unexpectedly set")となり、ユーザー提供リスナーが拒否される
仕組みになっている。