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

Go Proposal Weekly Digest

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

新機能

net/http/httptestにインメモリネットワークとtesting.TB連携を備えた新コンストラクタNewTestServerを追加する

net/http/httptest

この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 検証済み

概要

net/http/httptestパッケージに新しいコンストラクタ
NewTestServerが追加される。
NewTestServer(t testing.TB, handler http.Handler) *ServerはデフォルトでOSのネットワークスタックを
使わないインメモリネットワークを使用し、testing/synctest
パッケージと組み合わせて使えるHTTPテストサーバーを返す。testing.TBを受け取ることで、ハンドラが
パニックした場合の自動テスト失敗、およびテスト終了時のサーバー自動クリーンアップ(t.Cleanup)も
行われる。明示的にServer.StartまたはServer.StartTLSを呼べば、従来どおりループバックインター
フェース上の実TCPソケットを使う動作に切り替えられる。

導入経緯

提案者はhttptest.NewServerを多用しているが、OSのネットワークスタックを使うため
testing/synctestと組み合わせられないという課題を挙げ、net.Pipe()を手動で組み立てて
インメモリ接続を作る現状のワークアラウンドが複雑であることを指摘した(go.dev/play/p/AVXzqqwiJPn参照)。
当初の提案名はNewSynctestServer(handler http.Handler) *Serverだったが、レビューの過程で
「synctest固有の要素はない」という指摘を受けNewFakeNetServerに改名され、最終的には
「テストに適した推奨コンストラクタ」であることを示すNewTestServerに落ち着いた。長年未解決だった
net/http/httptest: optional faster test server #14200
とも実質的に同じ課題を扱っている。

議論タイムライン

  • 2026-04-15: Proposal Reviewミーティングでactiveカラムに追加され、週次レビューの対象となった。
  • 2026-05-06: @aclementsが"likely accept"と判定。APIはNewTestServer(t testing.TB, handler http.Handler) *Serverに収束した。
  • 2026-05-13: コンセンサスに変更がないことが確認され、正式に"accepted"となった。

議論のハイライト

  • 命名の変遷: NewSynctestServerNewFakeNetServerNewTestServer。synctest専用ではなく
    一般的なテスト向けサーバーという位置づけが最終的に採用された。
  • testing.TB引数はスコープクリープ: 当初の提案にはなかったが、ハンドラパニックの即時テスト
    失敗報告と自動クリーンアップという実用上のメリットが評価され採用された。net/http/httptest
    パッケージ名に"test"を含むため、testingパッケージへの依存追加は許容された。
  • インメモリ/ループバックの切り替え条件: Server.Clientを最初に呼ぶとインメモリネットワーク、
    Server.StartまたはServer.StartTLSを最初に呼ぶとループバックネットワークになるという遅延初期化
    方式が採用された。
  • ユーザー提供Listenerは非サポート: NewTestServerで作成したサーバーに独自のnet.Listener
    設定することは許可されず、将来の設計自由度を残すためパニックする仕様となった。
  • 複数サーバーでのフェイクネット共有は対象外: リバースプロキシのテストなどで複数サーバーが
    同一のフェイクネットワークを共有するケースは今回のスコープ外とされ、
    proposal: testing/nettest: add in-memory networking implementation #77362
    の進展を待つ方針となった。
  • 既存コンストラクタは維持: NewServer/NewTLSServer/NewUnstartedServerは後方互換のため残し、
    ドキュメント上で新規コードにはNewTestServerを推奨する形にとどめた。

使用例

Before

package httptest_test

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestHandler(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "hello")
	}))
	defer server.Close()

	resp, err := server.Client().Get(server.URL)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
}

After

package httptest_test

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"testing/synctest"
)

func TestHandler(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		server := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			fmt.Fprintln(w, "hello")
		}))

		// インメモリネットワークが使われるため、宛先ホスト名を問わずリクエストはserverに届く。
		resp, err := server.Client().Get("http://example.com/")
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()

		// サーバーはt.Cleanupで自動的にCloseされるので、defer server.Close()は不要。
	})
}

移行時の注意

  • NewTestServerが返すサーバーのServer.Listenerはインメモリネットワーク使用時には設定されない。
    独自のnet.Listenerを差し込みたい場合はNewTestServerではパニックするため、従来どおり
    NewUnstartedServerを使う必要がある。
  • インメモリネットワーク使用時、Server.Clientが返すhttp.Clientは宛先ホスト名やアドレスに
    関係なくすべてのリクエストをテストサーバーに送る。特定ホストのみに絞りたい既存のテストを
    移行する場合は挙動差に注意する。
  • 実ループバックネットワークを使い続けたい場合はServer.StartまたはServer.StartTLSを明示的に
    呼ぶ。この場合Server.Clientexample.comまたはそのサブドメイン宛のリクエストのみを
    サーバーへ転送する(既存のhttptest.NewServerとは異なりホスト名ベースの振り分けになる)。
  • handlernilを渡した場合、http.DefaultServeMuxは使われず、常に500応答を返すハンドラに
    なる。

実装解説

go-src(golang/go master)のsrc/net/http/httptest/server.goを確認した。

  • NewTestServer
    Config.Handlerにパニック処理用のtestServerHandlerをラップして設定し、
    t.Cleanup(func() { s.Close() })でサーバーの自動シャットダウンを登録するだけで、
    この時点ではネットワークの起動は行わない。
  • testServerHandler.ServeHTTP
    がハンドラのパニックをrecoverし、パニック値がhttp.ErrAbortHandler以外ならt.Errorf
    テストを失敗させたうえで、http.Server側の標準エラーへのログ出力を抑制するために
    http.ErrAbortHandlerとして再パニックする。
  • Server.Client
    s.t != nil(=NewTestServer経由で作られたサーバー)の場合のみ
    s.startOnce.Do(s.startFakeNet)で遅延初期化を行う。NewServerなど従来のコンストラクタでは
    tnilのためこの分岐は通らない。
  • startFakeNet
    internal/nettestnettest.ListenerをHTTP用・HTTPS用に1つずつ作成し、
    http.Transport.DialContext/DialTLSContextを宛先に関わらず常にそのフェイクリスナーへ
    接続する実装に差し替える。TLSクライアントにはInsecureSkipVerifyを設定し、
    Server.URLは固定で"http://example.com"となる。
  • Server.Start
    /StartTLS
    は共通処理startCommon
    を経由し、useLoopback=trueかつs.t != nilの場合はs.Listenerが未設定であることを確認した
    うえでnewLocalListener()が返す実TCPリスナーを設定する。すでにs.Listenerが設定されていると
    ここでpanic("Server.Listener is unexpectedly set")となり、ユーザー提供リスナーが拒否される
    仕組みになっている。

関連リンク