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

Go Proposal Weekly Digest

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

#76608likely_accept

net/http/httptest: synctest support

ステータス変更: active likely_accept

要約

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

概要

net/http/httptest パッケージに新しいコンストラクタ NewTestServer(t testing.TB, handler http.Handler) *Server を追加するproposalです。この関数はインメモリのフェイクネットワークをデフォルトで使用し、testing/synctest パッケージとの完全な互換性を実現しながら、ハンドラのパニック検出やテスト終了時の自動クリーンアップなど、テストの利便性を向上させます。

ステータス変更

activelikely_accept
2026年5月6日の週次Proposal Reviewミーティングにおいて、@aclements がこのProposalを "likely accept" と判定しました。最終的なAPIとして aclements が提案したドキュメントコメントを整備した NewTestServer(t testing.TB, handler http.Handler) *Server が採用される見込みとなりました。議論の焦点は名前の変遷(NewSynctestServerNewFakeNetServerNewTestServer)とAPIの設計(単一コンストラクタで複数の動作を制御する方針)にありました。

技術的背景

現状の問題点

testing/synctest パッケージはゴルーチンの実行と時間の進行を制御することで、非決定的なタイミングに依存するコードのテストを可能にします。しかし、synctest.Test と実際のネットワーク(OSのネットワークスタック)を組み合わせると、synctest の管理外のゴルーチンがネットワーク操作をブロックするため、デッドロックやタイムアウトが発生します。
httptest.NewServer は現在、localhost 上のTCPソケットを使用するため、synctest との組み合わせが困難です。回避策として net.Pipe() を使ったカスタム実装が必要でしたが、その実装は非自明で、ユーザーが独自に組み上げるには一定の知識が必要でした(Issue提案者の @rogpeppe も「考え出すのに少し時間がかかった」とコメント)。

// 現状のワークアラウンド(複雑でユーザーが手作業で組み上げる必要あり)
// https://go.dev/play/p/AVXzqqwiJPn 参照
// net.Pipe() でカスタムリスナーを作成し、httptest.Server に注入する

提案された解決策

新しいコンストラクタ NewTestServer を追加します:

// NewTestServer returns a new [Server] for a test.
//
// The Server will be started on the first call to [Server.Client], [Server.Start],
// or [Server.StartTLS].
//
// The Server may use an in-memory network implementation or a local
// network loopback interface. Calling [Server.Client] without Start or StartTLS
// causes the Server to use an in-memory network implementation.
//
// The in-memory network is suitable for use with the [testing/synctest] package,
// but does not require it.
//
// If the server handler panics with any value other than ErrAbortHandler,
// the test will fail.
//
// NewTestServer registers a test cleanup function to shut down the server.
// It is not necessary to call [Server.Close].
func NewTestServer(t testing.TB, handler http.Handler) *Server

デフォルトでインメモリネットワークを使用しますが、Start() または StartTLS() を明示的に呼ぶことでローカルループバックインターフェイス(実際のTCPソケット)に切り替えることができます。

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

コード例

// Before: synctest と httptest.NewServer の組み合わせは困難
// net.Pipe() を使ったカスタムリスナーを自前で実装する必要があった
func TestHandler_WithSynctest(t *testing.T) {
    synctest.Run(func() {
        // net.Pipe() からカスタムリスナーを手作業で組み上げる(非自明)
        // ...(複雑な初期化コード)...
    })
}
// After: NewTestServer でシンプルかつ synctest 対応
func TestHandler_WithSynctest(t *testing.T) {
    synctest.Run(func() {
        server := httptest.NewTestServer(t, myHandler)
        // server.Client() はフェイクネットワークを使用し、任意のホスト名でアクセス可能
        resp, err := server.Client().Get("http://example.com/path")
        // ...
    })
}
// ループバックネットワーク(実際のTCP)を使う場合
func TestHandler_WithLoopback(t *testing.T) {
    server := httptest.NewTestServer(t, myHandler)
    server.Start() // 明示的にStart()を呼ぶとループバックを使用
    resp, err := server.Client().Get(server.URL + "/path")
    // ...
}
// TLSを使う場合
func TestHandler_WithTLS(t *testing.T) {
    server := httptest.NewTestServer(t, myHandler)
    server.StartTLS()
    resp, err := server.Client().Get(server.URL + "/path")
    // ...
}

実践的なメリット:

  1. synctest との自動互換性: インメモリネットワークを使うことで、synctest.Run の内部でもデッドロックなしにHTTPサーバーをテスト可能になる。
  2. 自動クリーンアップ: t.Cleanupserver.Close() が自動登録されるため、テスト終了時のリソースリークが防止される。
  3. ハンドラのパニック検出: ハンドラが ErrAbortHandler 以外でパニックした場合、即座にテストを失敗させることができる(現行の httptest.NewServer ではパニックが見えにくい)。

議論のハイライト

  • 名前の変遷: 提案時の NewSynctestServer から NewFakeNetServer、最終的に NewTestServer へと変更。「synctest専用ではなく、一般的なテスト向けサーバー」というコンセプトに落ち着いた。synctest を使わないテストでもインメモリネットワークは有用なため。
  • testing.TB 引数の追加はスコープクリープだが価値あり: 当初のProposalには testing.T はなかったが、ハンドラパニックの即座な失敗通知・自動クリーンアップという実用的なメリットが評価され、最終設計に含まれた。httptest パッケージが testing パッケージに依存することになるが、パッケージ名に "test" が含まれているため問題ないと判断された。
  • フェイクネットワーク vs. ループバックの切り替えロジック: Client() を最初に呼ぶとインメモリネットワーク、Start() / StartTLS() を最初に呼ぶとループバックネットワーク(実TCPソケット)という仕様。遅延初期化(lazy start)の設計により、明示的な呼び出し順でネットワーク種別が決まる。
  • コンストラクタの増殖を防ぐ設計方針: NewTLSSynctestServer のような関数を増やす代わりに、単一の NewTestServer に設定変更を委ねる方針が採用された。既存の Server 構造体の可変フィールドを活用することで、将来の拡張(例: 複数サーバーによるフェイクネットワーク共有)にも対応できる。
  • 複数サーバー間のフェイクネット共有は将来課題: 1つのフェイクネット上で複数の httptest.Server を動作させるシナリオ(例: リバースプロキシのテスト)は、#77362(testing/nettest への汎用インメモリネット追加提案)の進展を待って対応する方針。現段階のProposalでは ServeMux を用いたホスト名ルーティングで代替可能。

関連リンク