net/http/httptest: synctest support
要約
概要
net/http/httptest パッケージに新しいコンストラクタ NewTestServer(t testing.TB, handler http.Handler) *Server を追加するproposalです。この関数はインメモリのフェイクネットワークをデフォルトで使用し、testing/synctest パッケージとの完全な互換性を実現しながら、ハンドラのパニック検出やテスト終了時の自動クリーンアップなど、テストの利便性を向上させます。
ステータス変更
active → likely_accept
2026年5月6日の週次Proposal Reviewミーティングにおいて、@aclements がこのProposalを "likely accept" と判定しました。最終的なAPIとして aclements が提案したドキュメントコメントを整備した NewTestServer(t testing.TB, handler http.Handler) *Server が採用される見込みとなりました。議論の焦点は名前の変遷(NewSynctestServer → NewFakeNetServer → NewTestServer)と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")
// ...
}
実践的なメリット:
- synctest との自動互換性: インメモリネットワークを使うことで、
synctest.Runの内部でもデッドロックなしにHTTPサーバーをテスト可能になる。 - 自動クリーンアップ:
t.Cleanupにserver.Close()が自動登録されるため、テスト終了時のリソースリークが防止される。 - ハンドラのパニック検出: ハンドラが
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を用いたホスト名ルーティングで代替可能。
関連リンク
- Proposal Issue github.com/golang/go
- Review Comment proposal review meeting
- Review Minutes
- 関連Issue #14200: net/http/httptest: optional faster test server
- 関連Issue #75294: net/http/httptest: ability to use net.Pipe() for testing/synctest (closed)
- 関連Issue #77362: proposal: testing/nettest: in-memory implementations of net package interfaces
- Proposal Issue