go / freeport
I use ephemeral port binding to find an available port for
integration tests that spawn a subprocess and need to hand it
the port via env var. For in-process HTTP tests, prefer
httptest.NewServer.
The pattern
import (
"fmt"
"net"
"testing"
)
func pickFreePort(t testing.TB) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen :0: %v", err)
}
port := fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port)
_ = ln.Close()
return port
}
Binding to port 0 asks the OS for any available ephemeral port. The listener is closed immediately so the subprocess can bind it.
Usage
When a test spawns a separate process (via
exec.CommandContext) that does its own listening:
func TestEndToEnd(t *testing.T) {
port := pickFreePort(t)
srvURL := "http://127.0.0.1:" + port
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
t.Cleanup(cancel)
cmd := exec.CommandContext(ctx, "go", "run", "./cmd/myserver")
cmd.Env = []string{"PORT=" + port}
if err := cmd.Start(); err != nil {
t.Fatalf("start: %v", err)
}
t.Cleanup(func() { _ = cmd.Process.Kill() })
// Poll until ready
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if resp, err := http.Get(srvURL + "/health"); err == nil {
resp.Body.Close()
break
}
time.Sleep(100 * time.Millisecond)
}
// ... actual test ...
}
For in-process HTTP tests, use httptest
If the server runs in the same process as the test, skip
this helper. httptest.NewServer allocates a port, starts
the server, and hands you a URL and a client:
srv := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(srv.Close)
resp, err := srv.Client().Get(srv.URL + "/path")
The free-port pattern is only for the case where something else does the binding.
Caveats
There's a TOCTOU (time-of-check vs. time-of-use) window
between ln.Close() and the subprocess binding to the port:
another process could grab it in between. On a developer or
CI machine this is rare, and a readiness poll catches the
failure mode quickly. For production servers, configure ports
explicitly instead.
When to use
- Subprocess integration tests where the child needs the port via env var or argument
- Any test where the listener can't be handed to the server
When not to use
- In-process HTTP tests (use
httptest.NewServer) - Production deployments (use explicit configuration)
- When the same listener can be passed to the server