メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

#76608accepted

net/http/httptest: synctest support

ステータス変更: likely_accept accepted

要約

AIによる要約であり、誤りを含む場合があります。

概要

net/http/httptest パッケージに NewTestServer(t testing.TB, handler http.Handler) *Server 関数を追加し、testing/synctest パッケージと互換性のあるインメモリネットワークを使ったHTTPテストサーバーを提供するproposalです。

ステータス変更

likely_acceptaccepted
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 を推奨」と明示する方針が決定。

関連リンク