How to Mock HTTP Calls in Go (httptest)

Mock HTTP calls in Go by using net/http/httptest to create in-memory servers and recorders for testing handlers without network I/O.

The problem with real servers in tests

You write an HTTP handler that returns JSON. You want to test it. The naive approach is to spin up a real server, send a request, and check the response. This works until the test depends on a database that isn't running, or an external API that changes its schema, or the network drops in CI. Tests should be fast, deterministic, and isolated. Relying on real network I/O makes tests flaky and slow.

You need a way to exercise the handler logic without touching the network stack. You need to control the request and inspect the response with precision. Go's standard library provides exactly this with net/http/httptest. It gives you tools to create requests and record responses in memory, or to start a temporary server that listens on a random port for testing clients.

httptest gives you an in-memory wire

The httptest package lets you simulate HTTP interactions without a real socket. You can construct a *http.Request directly and pass it to a handler. You can capture the output using a ResponseRecorder that implements http.ResponseWriter. Everything stays in memory. No port binding, no TCP handshake, no kernel overhead.

Think of httptest as a soundproof booth. You can shout a request into the booth and hear the response, but no one outside hears a thing. The booth records everything you say and everything the handler says back. You get full control over the input and complete visibility into the output.

The package also supports httptest.NewServer, which starts a real HTTP server in the background on a random available port. This is useful when you need to test code that makes HTTP requests, such as a client library or a middleware chain that expects a URL. The server runs in-memory and shuts down when you close it.

Minimal example: recorder and request

Here's the simplest pattern: create a request, create a recorder, run the handler, and assert the results.

// TestHandler verifies the response status and body for a simple handler.
func TestHandler(t *testing.T) {
	// Create a request with method and path. Body is nil here.
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	// Recorder captures the response written by the handler.
	rr := httptest.NewRecorder()

	// Wrap the handler function to satisfy the Handler interface.
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Write status and body to the recorder.
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("Hello"))
	})

	// Execute the handler with the request and recorder.
	handler.ServeHTTP(rr, req)

	// Check the status code.
	if rr.Code != http.StatusOK {
		t.Errorf("expected status %d, got %d", http.StatusOK, rr.Code)
	}
	// Check the body content.
	if rr.Body.String() != "Hello" {
		t.Errorf("expected body 'Hello', got %q", rr.Body.String())
	}
}

The recorder is your witness. It captures everything the handler writes so you can verify it later.

How the recorder works

httptest.NewRecorder returns a *httptest.ResponseRecorder. This struct implements http.ResponseWriter, so you can pass it anywhere a handler expects a response writer. It buffers the status code, headers, and body in memory.

After ServeHTTP returns, you can inspect rr.Code for the status, rr.Header() for the headers, and rr.Body for the response body. The body is an *bytes.Buffer, so you can call String() to get the text, or Bytes() to get the raw slice.

The recorder does not flush headers automatically. If the handler writes the body before calling WriteHeader, the recorder handles this correctly by setting the default status code. This matches the behavior of the real HTTP server.

A common convention in Go is to use http.HandlerFunc to adapt a function to the Handler interface. This adapter is part of the standard library and appears in almost every Go web project. It lets you write handlers as plain functions without defining a custom struct type.

Realistic example: JSON and headers

Real handlers often return JSON, set content types, and parse query parameters. Here's a test that checks all of that.

// TestGetUserHandler validates JSON output and query parsing.
func TestGetUserHandler(t *testing.T) {
	// Request includes a query parameter for the user ID.
	req := httptest.NewRequest(http.MethodGet, "/users?id=42", nil)
	rr := httptest.NewRecorder()

	// Handler extracts ID and returns JSON.
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.URL.Query().Get("id")
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"id": "` + id + `"}`))
	})

	handler.ServeHTTP(rr, req)
}

The handler runs and writes to the recorder. Now you check the results. The recorder buffers everything, so you can inspect the status, headers, and body after the call returns.

// Continue assertions after ServeHTTP returns.
// Assert status code matches expectation.
if rr.Code != http.StatusOK {
	t.Fatalf("expected 200, got %d", rr.Code)
}
// Assert content type header is set correctly.
if rr.Header().Get("Content-Type") != "application/json" {
	t.Errorf("expected json content type, got %q", rr.Header().Get("Content-Type"))
}
// Assert body contains the expected ID value.
body := rr.Body.String()
if !strings.Contains(body, `"id": "42"`) {
	t.Errorf("expected id 42 in body, got %s", body)
}

Headers and status codes matter as much as the body. Check them all.

Testing clients with a mock server

Sometimes you need to test code that makes HTTP requests. For example, you might have a client that calls your API, or a middleware that proxies requests. In these cases, httptest.NewServer is the right tool. It starts a real server on a random port and gives you a URL to pass to the client.

// TestClientWithServer demonstrates testing a client against a mock server.
func TestClientWithServer(t *testing.T) {
	// Start a server that returns a fixed response.
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("mocked response"))
	}))
	// Ensure the server is closed after the test.
	t.Cleanup(server.Close)

	// Create a client that talks to the mock server.
	client := &http.Client{}
	resp, err := client.Get(server.URL)
	if err != nil {
		t.Fatalf("client request failed: %v", err)
	}
	t.Cleanup(resp.Body.Close)
}

The server runs on a random available port. server.URL gives you the full address. The client makes a real TCP connection to this in-memory server. This tests the client's HTTP logic without hitting the real backend.

// Verify the client received the mocked data.
body, err := io.ReadAll(resp.Body)
if err != nil {
	t.Fatalf("failed to read body: %v", err)
}
if string(body) != "mocked response" {
	t.Errorf("expected mocked response, got %q", string(body))
}

Use t.Cleanup to register the server close function. This is the modern Go testing convention. t.Cleanup runs after the test function returns, even if the test fails. It also runs after subtests, which makes it safer than defer in complex test suites.

If you forget to close the server, you leak a port. The test runner won't catch this, but your CI might fail with "address already in use" after many runs. Always close what you start.

Pitfalls and conventions

Mocking HTTP in Go is straightforward, but there are a few traps to avoid.

Forgetting to check rr.Code is a common mistake. Handlers might write a body but fail to set the status code. The recorder defaults to 200, so your test passes even if the handler returns an error. Always assert the status code explicitly.

Using httptest.NewServer when NewRecorder is enough adds unnecessary overhead. NewServer starts a TCP listener and spawns a goroutine. It's slower than the recorder pattern. Use the recorder for unit tests of handlers. Use the server only when you need a URL or when testing clients.

If you try to use http.NewRequest in a test without a context, the compiler might not complain, but it's bad practice. http.NewRequest is designed for production code where you have a context. httptest.NewRequest is the test helper. It doesn't require a context and sets up the URL properly for testing. If you pass a malformed URL to httptest.NewRequest, the function returns a nil request and an error. Check the error or use t.Fatalf.

The compiler rejects this with a nil-pointer dereference panic if you ignore the error and call methods on a nil request. Always handle the error or use the helper that doesn't return one for simple cases.

A convention in Go is to prefer standard library helpers over external mocking frameworks. httptest is part of the standard library. It's well-tested, fast, and requires no dependencies. You rarely need a third-party library to mock HTTP calls in Go.

Another convention is receiver naming. If your handler is a method on a struct, use a short receiver name that matches the type. (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) is idiomatic. Avoid (this *Handler) or (self *Handler). Go doesn't use this or self keywords.

When to use what

Pick the tool that matches the boundary you want to test. Isolation is fast. Integration is thorough.

Use httptest.NewRequest and httptest.NewRecorder when you want to test a single handler in isolation without network overhead.

Use httptest.NewServer when you need to test code that makes HTTP requests to your handler, such as a client library or middleware chain.

Use httptest.NewUnstartedServer when you need to configure the server options, like TLS or timeouts, before it starts listening.

Use a real integration test against a running database or service when you need to verify the full stack, including network behavior and external dependencies.

Where to go next