net/http/httptest: synctest support
要約
概要
net/http/httptest パッケージに NewTestServer(t testing.TB, handler http.Handler) *Server 関数を追加し、testing/synctest パッケージと互換性のあるインメモリネットワークを使ったHTTPテストサーバーを提供するproposalです。
ステータス変更
likely_accept → accepted
likely_accept の段階で提示された API仕様(NewTestServer + testing.TB 引数)に対してコアチームからの反対意見はなく、@neild が詳細なドキュメントコメントと実装CL(go.dev/cl/769521)を提出したことで合意が確定しました。@aclements が2026年5月13日にコンセンサス変更なしと確認し、正式に accepted となりました。
技術的背景
現状の問題点
testing/synctest パッケージは、並行コードのテストにおいてgoroutineの同期を制御できる仕組みですが、実際のOSネットワークスタック(TCP接続)を使う httptest.NewServer とは根本的に相性が悪い問題があります。synctest はファイクの時間進行やgoroutineの休止検知を行うため、実際のソケットI/Oがあると制御できない非決定的な動作が混入します。
現状のワークアラウンドとして、net.Pipe() を使って手動でインメモリ接続を構築する方法がありますが(提案者の go.dev/play/p/AVXzqqwiJPn 参照)、それを httptest.Server と組み合わせる公式APIが存在せず、複雑なセットアップが必要でした。また、実際のTCPを使ったテストでは「エフェメラルポートの枯渇」という問題が発生することもあります。
提案された解決策
新しい関数 NewTestServer を追加します:
func NewTestServer(t testing.TB, handler http.Handler) *Server
この関数の主な特徴:
- デフォルトでインメモリネットワークを使用(TCPポートを消費しない)
Server.Client()を呼ぶだけでサーバーが起動し、全てのHTTP/HTTPSリクエストを宛先に関係なくそのサーバーに向けるtesting.TBを受け取ることで、ハンドラのパニック時に自動的にテスト失敗を記録し、テスト終了時にCleanupでサーバーを自動シャットダウンServer.Start()またはServer.StartTLS()を明示的に呼ぶと、ループバックインターフェース(実際のTCP)を使用するモードに切り替わる
これによって何ができるようになるか
testing/synctest を使った時間制御テストや非同期処理のテストに、フルHTTPスタックのテストサーバーをシームレスに組み合わせることが可能になります。
コード例
// Before: 従来の書き方(synctest非対応、ポート枯渇の懸念あり)
func TestHandler(t *testing.T) {
server := httptest.NewServer(myHandler)
defer server.Close()
resp, err := server.Client().Get(server.URL + "/path")
// ...
}
// After: NewTestServer を使った書き方(synctest対応、自動クリーンアップ)
func TestHandler(t *testing.T) {
// インメモリネットワークを使用。synctest.Run内でも動作する。
// サーバーはt.Cleanupで自動シャットダウン。
server := httptest.NewTestServer(t, myHandler)
// 宛先ホスト名・アドレスに関係なく全リクエストがこのサーバーに転送される
resp, err := server.Client().Get("http://www.example.com/path")
// ...
}
// After: ループバックTCPを使いたい場合(明示的にStartを呼ぶ)
func TestHandlerWithLoopback(t *testing.T) {
server := httptest.NewTestServer(t, myHandler)
server.Start() // 明示的呼び出しでTCPループバックに切り替わる
resp, err := server.Client().Get(server.URL + "/path")
// ...
}
// After: synctest.Run と組み合わせた例
func TestHandlerTimeout(t *testing.T) {
synctest.Run(func() {
server := httptest.NewTestServer(t, timeoutHandler)
_, err := server.Client().Get("http://example.com/slow")
// synctest によって時間を進められる
})
}
議論のハイライト
- 命名の変遷: 最初は
NewSynctestServer(提案者@rogpeppe)、次にNewFakeNetServer(@neild)、最終的にNewTestServerに落ち着いた。FakeNetはインメモリであることを強調するが、TestServerは「テストに適した推奨コンストラクタ」という位置づけを明確にする。 testing.TB引数の追加(スコープ拡大): 当初testing.Tの依存なしで提案されていたが、@aclementsがハンドラパニックの自動テスト失敗報告・サーバー自動クリーンアップを実現するためにtesting.TBの受け取りを提案した。net/http/httptestは名前に「test」を含むため依存追加は許容された。- TLS対応の設計: インメモリモードでHTTPとHTTPS両方を自動サポートするか議論された。最終的に、インメモリモードでは
Client()が全リクエスト(HTTP/HTTPS)をサーバーに向け、ループバックモードではStart()かStartTLS()の呼び出しで明示的に切り替える設計が採用された。 - 複数サーバー間の連携: 同一テスト内で複数のインメモリサーバーを使う場合(例: ReverseProxy テスト)のサポートは今回スコープ外とされた。将来の
#77362(汎用フェイクネットワーク提案)で対応予定。 - 既存APIの位置づけ:
NewServer/NewTLSServer/NewUnstartedServerは後方互換性のため残すが、ドキュメント上は「新規コードではNewTestServerを推奨」と明示する方針が決定。
関連リンク
- Proposal Issue github.com/golang/go
- Review Comment proposal review meeting
- 関連Issue #14200: net/http/httptest: optional faster test server
- 関連Issue #77362: proposal: testing/nettest: in-memory implementations of net package interfaces
- 関連Issue #75294: proposal: net/http/httptest: ability to use net.Pipe() for testing/synctest
- Proposal Issue
- Review Minutes