Documentation
¶
Overview ¶
Package httpmock provides a simple, declarative API for testing HTTP clients.
The package centers around the Server type, which wraps httptest.Server to provide a way to specify expected HTTP requests and their corresponding responses. This makes it easy to verify that your HTTP client makes the expected calls in the expected order.
Basic usage:
server := httpmock.NewServer(t, []httpmock.Exchange{{
Request: httpmock.Request{
Method: "GET",
Path: "/api/users",
Headers: map[string]string{
"Authorization": "Bearer token123",
},
},
Response: httpmock.Response{
StatusCode: http.StatusOK,
Body: map[string]interface{}{
"users": []string{"alice", "bob"},
},
},
}})
defer server.Close()
// Use server.Path("/api/users") as the base URL for your HTTP client...
The Server will verify that requests match the expected method, path, headers, and body (if specified). For bodies, it supports both exact string matches and JSON comparison. You can also provide custom validation functions for more complex request validation:
Request: httpmock.Request{
Method: "POST",
Path: "/api/users",
Validate: func(r *http.Request) error {
if r.Header.Get("Content-Length") == "0" {
return fmt.Errorf("empty request body")
}
return nil
},
}
You can simulate network latency or test timeout handling by adding a delay to responses:
Response: httpmock.Response{
StatusCode: http.StatusOK,
Body: `{"success": true}`,
Delay: 2 * time.Second, // Response will be delayed by 2 seconds
}
After your test completes, calling Close will shut down the server and verify that all expected requests were received:
server := httpmock.NewServer(t, []httpmock.Exchange{...})
defer server.Close() // will fail the test if not all expectations were met
The server will automatically verify all expectations when Close() is called.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewReplayClient ¶
func NewReplayClient(tester T, config ReplayConfig) (*http.Client, func())
NewReplayClient creates a pre-configured http.Client that records and replays HTTP interactions. This is a convenience function that creates a ReplayTransport and wraps it in an http.Client.
Example usage:
client, close := httpmock.NewReplayClient(t, httpmock.ReplayConfig{
Cassette: "my_test_cassette",
})
defer close()
// Use client for requests to any hosts - they'll be recorded/replayed
resp, err := client.Get("https://api.example.com/users")
Example ¶
ExampleNewReplayClient demonstrates the convenience function for creating a pre-configured HTTP client for recording and replaying requests.
mockT := &t{} // In a real test, this would be the actual testing.T instance
// Create a pre-configured HTTP client - one line!
client, close := httpmock.NewReplayClient(mockT, httpmock.ReplayConfig{
Cassette: "testdata/transport_client_example",
})
defer close()
// Use the client directly - no transport configuration needed
resp, err := client.Get("https://httpbin.org/get")
require.NoError(mockT, err)
defer func() { _ = resp.Body.Close() }()
fmt.Printf("Response status: %d\n", resp.StatusCode)
Output: Response status: 200
Types ¶
type ReplayConfig ¶
type ReplayConfig struct {
// Host is the address of the real endpoints we're proxying requests to,
// e.g. "https://api.example.com".
Host string
// Cassette is the name of the cassette file to use for recording and replaying.
// If the cassette does not exist, it will be created.
// Do not include the ".yaml" extension, it will be added automatically.
Cassette string
// Mode determines whether to use cassettes (unit testing) or always hit the real API (integration)
// If not specified, defaults to the value of INTEGRATION environment variable
//
// Possible values are "unit" and "integration".
Mode TestMode
// AdditionalIgnoredHeaders specifies additional headers to ignore during recording and replay.
// These headers will be added to the default set of ignored headers.
// Header names are case-insensitive.
AdditionalIgnoredHeaders []string
}
ReplayConfig is the configuration for a ReplayServer.
type ReplayServer ¶
type ReplayServer struct {
// contains filtered or unexported fields
}
ReplayServer is a server that can be used to test HTTP interactions by recording and replaying real HTTP requests.
When a test is first run, the ReplayServer will record the interactions and save them to a "cassette" file.
On subsequent runs, the ReplayServer will replay the interactions from the cassette file, allowing for consistent tests.
Example (Basic) ¶
ExampleReplayServer_basic demonstrates basic usage of the ReplayServer for recording and replaying HTTP interactions with httpbin.org.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/stretchr/testify/require"
"go.jetify.com/pkg/httpmock"
)
// t is a simple implementation that satisfies the testing.TB interface
// requirements for the examples, but is separate from the mockT in the test file.
type t struct{}
func (*t) Errorf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+format+"\n", args...)
}
func (*t) FailNow() {}
func (*t) Helper() {}
func main() {
mockT := &t{} // In a real test, this would be the actual testing.T instance
// Create a new replay server that will record interactions with httpbin.org
replayServer := httpmock.NewReplayServer(mockT, httpmock.ReplayConfig{
Host: "https://httpbin.org",
Cassette: "testdata/server_successful_get",
})
defer replayServer.Close()
// Make a request that will be recorded or replayed
req, err := http.NewRequest(http.MethodGet, replayServer.URL()+"/get", nil)
require.NoError(mockT, err)
req.Host = "httpbin.org"
req.Header.Set("Accept-Encoding", "gzip")
resp, err := http.DefaultClient.Do(req)
require.NoError(mockT, err)
defer func() { _ = resp.Body.Close() }()
// Read and print the response status and body
body, err := io.ReadAll(resp.Body)
require.NoError(mockT, err)
// Parse the JSON response
var result map[string]interface{}
err = json.Unmarshal(body, &result)
require.NoError(mockT, err)
// Print only the status code
fmt.Printf("Status: %d\n", resp.StatusCode)
}
Output: Status: 200
Example (Headers) ¶
ExampleReplayServer_headers demonstrates how ReplayServer handles headers while recording and replaying HTTP interactions.
package main
import (
"fmt"
"net/http"
"github.com/stretchr/testify/require"
"go.jetify.com/pkg/httpmock"
)
// t is a simple implementation that satisfies the testing.TB interface
// requirements for the examples, but is separate from the mockT in the test file.
type t struct{}
func (*t) Errorf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+format+"\n", args...)
}
func (*t) FailNow() {}
func (*t) Helper() {}
func main() {
mockT := &t{} // In a real test, this would be the actual testing.T instance
replayServer := httpmock.NewReplayServer(mockT, httpmock.ReplayConfig{
Host: "https://httpbin.org",
Cassette: "testdata/server_get_with_header",
})
defer replayServer.Close()
// Create a request with custom headers
req, err := http.NewRequest(http.MethodGet, replayServer.URL()+"/headers", nil)
require.NoError(mockT, err)
req.Host = "httpbin.org"
req.Header.Set("X-Test-Header", "test-value")
req.Header.Set("Accept-Encoding", "gzip")
// Make the request
resp, err := http.DefaultClient.Do(req)
require.NoError(mockT, err)
defer func() { _ = resp.Body.Close() }()
// Print response status
fmt.Printf("Status: %d\n", resp.StatusCode)
}
Output: Status: 200
Example (JsonRequest) ¶
ExampleReplayServer_jsonRequest demonstrates handling JSON requests with ReplayServer.
package main
import (
"fmt"
"net/http"
"strings"
"github.com/stretchr/testify/require"
"go.jetify.com/pkg/httpmock"
)
// t is a simple implementation that satisfies the testing.TB interface
// requirements for the examples, but is separate from the mockT in the test file.
type t struct{}
func (*t) Errorf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+format+"\n", args...)
}
func (*t) FailNow() {}
func (*t) Helper() {}
func main() {
mockT := &t{} // In a real test, this would be the actual testing.T instance
replayServer := httpmock.NewReplayServer(mockT, httpmock.ReplayConfig{
Host: "https://httpbin.org",
Cassette: "testdata/server_post_with_body",
})
defer replayServer.Close()
// Create a JSON request
jsonData := `{"test": "data"}`
req, err := http.NewRequest(http.MethodPost, replayServer.URL()+"/post", strings.NewReader(jsonData))
require.NoError(mockT, err)
req.Host = "httpbin.org"
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept-Encoding", "gzip")
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonData)))
// Make the request
resp, err := http.DefaultClient.Do(req)
require.NoError(mockT, err)
defer func() { _ = resp.Body.Close() }()
// Print response status
fmt.Printf("Status: %d\n", resp.StatusCode)
}
Output: Status: 200
func NewReplayServer ¶
func NewReplayServer(tester T, config ReplayConfig) *ReplayServer
NewReplayServer creates a new ReplayServer.
func (*ReplayServer) Close ¶
func (rs *ReplayServer) Close()
Close stops the ReplayServer and verifies that all recorded interactions were used.
func (*ReplayServer) URL ¶
func (rs *ReplayServer) URL() string
URL returns the base URL of the replay server.
type ReplayTransport ¶
type ReplayTransport struct {
// contains filtered or unexported fields
}
ReplayTransport implements http.RoundTripper for recording and replaying HTTP interactions across multiple hosts. Unlike ReplayServer which acts as a proxy, ReplayTransport can be used directly as the Transport for an http.Client, allowing your code to make requests to multiple different hosts while recording/replaying all interactions.
When a test is first run, the ReplayTransport will record the interactions and save them to a "cassette" file.
On subsequent runs, the ReplayTransport will replay the interactions from the cassette file, allowing for consistent tests.
Example ¶
ExampleReplayTransport demonstrates using ReplayTransport to record and replay HTTP interactions across multiple hosts. This is ideal for testing SDKs that communicate with multiple different services.
mockT := &t{} // In a real test, this would be the actual testing.T instance
// Create a ReplayTransport that can record/replay requests to any host
transport := httpmock.NewReplayTransport(mockT, httpmock.ReplayConfig{
Cassette: "testdata/transport_multi_host_example",
})
defer transport.Close()
// Create an HTTP client that uses our transport
client := &http.Client{Transport: transport}
// Now the client can make requests to different hosts, and they'll all be
// recorded/replayed from the same cassette
// Request to first service
resp1, err := client.Get("https://httpbin.org/get")
require.NoError(mockT, err)
require.NoError(mockT, resp1.Body.Close())
// Request to second service
resp2, err := client.Get("https://jsonplaceholder.typicode.com/posts/1")
require.NoError(mockT, err)
require.NoError(mockT, resp2.Body.Close())
fmt.Printf("First service status: %d\n", resp1.StatusCode)
fmt.Printf("Second service status: %d\n", resp2.StatusCode)
Output: First service status: 200 Second service status: 200
func NewReplayTransport ¶
func NewReplayTransport(tester T, config ReplayConfig) *ReplayTransport
NewReplayTransport creates a new ReplayTransport that can be used as an http.RoundTripper for recording and replaying HTTP interactions.
Example usage:
transport := httpmock.NewReplayTransport(t, httpmock.ReplayConfig{
Cassette: "my_test_cassette",
Mode: httpmock.ModeUnitTest,
})
defer transport.Close()
client := &http.Client{Transport: transport}
// Now use client for requests to any hosts - they'll be recorded/replayed
func (*ReplayTransport) Close ¶
func (rt *ReplayTransport) Close()
Close stops the ReplayTransport and verifies that all recorded interactions were used. This should be called when the test is complete, typically in a defer statement.
type Request ¶
type Request struct {
Body any // optional: if non-empty, we check the body as JSON (or raw string)
Form url.Values
Headers map[string]string
Path string
Method string
// Validate is an optional callback for additional checks.
Validate func(r *http.Request) error
}
Request specifies what an incoming HTTP request should look like.
func MergeRequests ¶
MergeRequests creates a new Request by combining multiple requests. Later requests override values from earlier requests. Non-zero/non-empty values from each request override values from previous requests.
type Response ¶
type Response struct {
Body any // can be a JSON-marshalable object or a string
Headers map[string]string // optional: headers to include in response
Status string
StatusCode int // HTTP status code (defaults to 200 OK if not set)
Delay time.Duration // optional: delay before sending the response (for testing timeouts, etc.)
}
Response specifies how to respond to a matched request.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server provides a declarative API on top of httptest.Server for testing HTTP clients. It allows you to specify a sequence of expected requests and their corresponding responses, making it easy to verify that your HTTP client makes the expected calls in the expected order.
Example (Basic) ¶
ExampleServer_basic demonstrates basic usage of the httpmock Server.
package main
import (
"fmt"
"io"
"net/http"
"go.jetify.com/pkg/httpmock"
)
// t is a simple implementation that satisfies the testing.TB interface
// requirements for the examples, but is separate from the mockT in the test file.
type t struct{}
func (*t) Errorf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+format+"\n", args...)
}
func (*t) FailNow() {}
func (*t) Helper() {}
func main() {
testServer := httpmock.NewServer(&t{}, []httpmock.Exchange{{
Request: httpmock.Request{
Method: "GET",
Path: "/hello",
},
Response: httpmock.Response{
Body: "world",
Headers: map[string]string{
"Content-Type": "text/plain",
},
},
}})
defer testServer.Close()
resp, _ := http.Get(testServer.Path("/hello"))
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Output: world
Example (Delay) ¶
ExampleServer_delay demonstrates response delay functionality.
package main
import (
"fmt"
"net/http"
"time"
"go.jetify.com/pkg/httpmock"
)
// t is a simple implementation that satisfies the testing.TB interface
// requirements for the examples, but is separate from the mockT in the test file.
type t struct{}
func (*t) Errorf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+format+"\n", args...)
}
func (*t) FailNow() {}
func (*t) Helper() {}
func main() {
// Create a server with a delayed response
testServer := httpmock.NewServer(&t{}, []httpmock.Exchange{{
Request: httpmock.Request{
Method: "GET",
Path: "/api/slow",
},
Response: httpmock.Response{
StatusCode: http.StatusOK,
Body: `{"status":"success", "data":"worth the wait"}`,
Delay: 1 * time.Second, // Response will be delayed by 1 second
},
}})
defer testServer.Close()
// Print the configured delay
fmt.Println("Configured delay:", 1*time.Second)
// Make the request
resp, err := http.Get(testServer.Path("/api/slow"))
if err != nil {
fmt.Println("request error:", err)
return
}
defer func() { _ = resp.Body.Close() }()
}
Output: Configured delay: 1s
Example (JsonRequest) ¶
ExampleServer_jsonRequest demonstrates handling JSON requests and responses.
package main
import (
"fmt"
"io"
"net/http"
"strings"
"go.jetify.com/pkg/httpmock"
)
// t is a simple implementation that satisfies the testing.TB interface
// requirements for the examples, but is separate from the mockT in the test file.
type t struct{}
func (*t) Errorf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+format+"\n", args...)
}
func (*t) FailNow() {}
func (*t) Helper() {}
func main() {
testServer := httpmock.NewServer(&t{}, []httpmock.Exchange{{
Request: httpmock.Request{
Method: "POST",
Path: "/api/users",
Body: `{"name":"Alice"}`,
Headers: map[string]string{
"Content-Type": "application/json",
},
},
Response: httpmock.Response{
StatusCode: http.StatusCreated,
Body: map[string]interface{}{
"id": 1,
"name": "Alice",
},
},
}})
defer testServer.Close()
resp, _ := http.Post(testServer.Path("/api/users"),
"application/json",
strings.NewReader(`{"name":"Alice"}`))
body, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode)
fmt.Println(string(body))
}
Output: 201 {"id":1,"name":"Alice"}
Example (Sequence) ¶
ExampleServer_sequence demonstrates a sequence of requests and responses.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go.jetify.com/pkg/httpmock"
)
// t is a simple implementation that satisfies the testing.TB interface
// requirements for the examples, but is separate from the mockT in the test file.
type t struct{}
func (*t) Errorf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+format+"\n", args...)
}
func (*t) FailNow() {}
func (*t) Helper() {}
func main() {
testServer := httpmock.NewServer(&t{}, []httpmock.Exchange{
{
Request: httpmock.Request{
Method: "POST",
Path: "/login",
Body: `{"username":"alice","password":"secret"}`,
},
Response: httpmock.Response{
Body: map[string]string{"token": "abc123"},
},
},
{
Request: httpmock.Request{
Method: "GET",
Path: "/profile",
Headers: map[string]string{
"Authorization": "Bearer abc123",
},
},
Response: httpmock.Response{
Body: map[string]string{"name": "Alice"},
},
},
})
defer testServer.Close()
// Login
resp, _ := http.Post(testServer.Path("/login"),
"application/json",
strings.NewReader(`{"username":"alice","password":"secret"}`))
var loginResp struct{ Token string }
err := json.NewDecoder(resp.Body).Decode(&loginResp)
if err != nil {
fmt.Println("decode error:", err)
return
}
_ = resp.Body.Close()
// Get profile using token
req, _ := http.NewRequest(http.MethodGet, testServer.Path("/profile"), nil)
req.Header.Set("Authorization", "Bearer "+loginResp.Token)
resp, _ = http.DefaultClient.Do(req)
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Output: {"name":"Alice"}
Example (Validation) ¶
ExampleServer_validation demonstrates custom request validation.
package main
import (
"fmt"
"io"
"net/http"
"strings"
"go.jetify.com/pkg/httpmock"
)
// t is a simple implementation that satisfies the testing.TB interface
// requirements for the examples, but is separate from the mockT in the test file.
type t struct{}
func (*t) Errorf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+format+"\n", args...)
}
func (*t) FailNow() {}
func (*t) Helper() {}
func main() {
testServer := httpmock.NewServer(&t{}, []httpmock.Exchange{{
Request: httpmock.Request{
Method: "POST",
Path: "/upload",
Validate: func(r *http.Request) error {
if r.Header.Get("Content-Length") == "0" {
return fmt.Errorf("empty request body")
}
return nil
},
},
Response: httpmock.Response{
StatusCode: http.StatusOK,
Body: "uploaded",
Headers: map[string]string{
"Content-Type": "text/plain",
},
},
}})
defer testServer.Close()
// Send non-empty request
resp, _ := http.Post(testServer.Path("/upload"),
"text/plain",
strings.NewReader("some data"))
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Output: uploaded
func NewServer ¶
NewServer creates and starts an httptest.Server that will match incoming requests to the provided expectations in order.
func (*Server) Close ¶
func (s *Server) Close()
Close shuts down the underlying httptest.Server and verifies all expected exchanges were completed.
func (*Server) Path ¶
Path returns the complete URL for the given path. For example, if the server URL is "http://localhost:12345" and path is "/api/users", this returns "http://localhost:12345/api/users". url.JoinPath handles all path normalization, including handling of leading/trailing slashes.
type T ¶
type T interface {
Errorf(format string, args ...interface{})
FailNow()
Helper()
}
T is an interface that captures the testing.T methods we need