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

Go Proposal Weekly Digest

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

#76608active

net/http/httptest: synctest support

新規提案

要約

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

概要

net/http/httptest パッケージに testing/synctest と連携して動作する HTTP テストサーバーを作成する機能を追加するプロポーザルです。現在の httptest.NewServer はOS のネットワークスタック(実際のTCPソケット)を使用するため testing/synctest と組み合わせることができず、この問題を解決する新しいAPIの追加を提案しています。

ステータス変更

(新規)active
2026年4月15日のProposal Review Meeting(@aclements, @adonovan, @cherrymui, @griesemer, @ianlancetaylor, @neild, @rolandshoemaker 参加)において、activeカラムに追加され、週次のProposal Reviewミーティングで正式に審議されることになりました。議事録では「added to minutes(議事録に追加)」と記録されており、議論が継続中であることが示されています。

技術的背景

現状の問題点

testing/synctest パッケージ(Go 1.25で正式導入)は、並行コードのテストのために「フェイククロック」と「バブル(goroutineグループ)」の概念を使用します。synctest バブル内でのgoroutineは全て「アイドル状態」になったときにのみ時刻が進む仕組みですが、ネットワーク I/O 操作(実際のTCPソケットの読み取りなど)を行うgoroutineは「アイドル」とみなされません。
そのため、httptest.NewServer で立ち上げたサーバーに対してHTTPリクエストを送ると、synctest バブルが永久にブロックされてしまうか、時間が期待通りに進みません。

// 問題のあるコード: synctest.Run の中で httptest.NewServer を使用
synctest.Run(func() {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(1 * time.Second) // synctest のフェイククロックで進むはず
        fmt.Fprintln(w, "Hello")
    }))
    defer ts.Close()
    before := time.Now()
    http.Get(ts.URL) // 実際のTCPを使うため、synctest と非互換 → ブロックまたは失敗
    after := time.Now()
    // d != 1s になる(synctest のフェイククロックが機能しない)
})

現状のワークアラウンドとしては net.Pipe() を手動でセットアップしてHTTPサーバーを構築する方法がありますが、これは非常に複雑で、提案者(@rogpeppe)自身も「少し時間がかかった」と述べています。

提案された解決策

提案者(@rogpeppe)は以下の新しい関数の追加を提案しています。

// NewSynctestServer は testing/synctest と組み合わせて使用するための
// サーバーを返して起動します。OSのネットワークスタックを使用しないため、
// net.Dial 等でサーバーにアクセスすることはできません。
// Server.Client() が返す http.Client を使用してください。
func NewSynctestServer(handler http.Handler) *Server

このサーバーはインメモリのフェイクネットワーク接続を使用し、OSのTCPスタックを経由しないため、synctest バブル内で正常に動作します。

これによって何ができるようになるか

httptest.NewServer の代わりに httptest.NewSynctestServer を使うことで、synctest バブル内でHTTPサーバーのタイムアウト処理、ミドルウェアのリトライロジック、WebSocketなどの長期接続の挙動をフェイククロックで制御しながらテストできるようになります。

コード例

// Before: net.Pipe を手動でセットアップする複雑なワークアラウンド
synctest.Run(func() {
    // net.Pipe(), カスタムDialer, httptest.Server の手動設定が必要
    // 実装が複雑で、初見では理解しにくい
})
// After: 新APIを使った書き方(提案中)
synctest.Run(func() {
    ts := httptest.NewSynctestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(1 * time.Second) // synctest のフェイククロックが正しく機能する
        fmt.Fprintln(w, "Hello")
    }))
    defer ts.Close()
    client := ts.Client() // net.Dial の代わりにこのクライアントを使う
    before := time.Now()
    client.Get(ts.URL)
    after := time.Now()
    // after.Sub(before) == 1s として正しく検証できる
})

議論のハイライト

  • 命名について: 提案者は NewSynctestServer という名前を提案しましたが、@seankhliao から「synctest 固有の要素は実はない」との指摘がありました。NewMemServer 等の代替名も検討されましたが、目的(高速化ではなくテスタビリティ向上)が異なるためこの名前が選ばれた経緯があります。
  • 既存 Issue との関係: 本提案は長年未解決の Issue #14200「httptest.Server のインメモリネットワーク対応」と実質的に同じ内容であることが指摘されており、どちらのIssueを正式なプロポーザルとして使うべきか検討中です。
  • API設計の選択肢: @neild は FakeNet bool フィールドを Server 構造体に追加する案を提示しましたが、NewSynctestServer(handler) という簡潔な関数呼び出し形式の方が好まれています。また、将来的にTLS版(NewTLSSynctestServer)が必要になった場合のAPI増殖問題も懸念として挙がっています。
  • 関連する並行提案: @neild により Issue #77362「testing/nettest パッケージへのインメモリネットワーク実装追加」という関連プロポーザルが提案されており、そちらのプリミティブ上に本機能を構築できる可能性があります。
  • NewUnstartedServer の問題: @seankhliao が提案した httptest.NewUnstartedServer(handler).FakeNet() というアプローチは、NewUnstartedServer が内部でTCPリスナーを作成してしまうため非効率であると @neild が指摘しました。

関連リンク