The ghttp package provides a fake HTTP server for testing HTTP clients. It allows you to define canned responses, verify that requests match expectations, and simulate various HTTP scenarios in your tests.
Package: github.com/onsi/gomega/ghttp
The ghttp package is designed for testing HTTP clients and services that make HTTP requests. It provides:
{ .api }
type Server struct {
// Contains filtered or unexported fields
}A fake HTTP server for testing. The server manages a queue of handlers and automatically verifies that all expected requests are received.
{ .api }
func NewServer() *ServerCreates and starts a new HTTP test server listening on an automatically assigned port.
Returns: *Server - A started HTTP server
Example:
server := ghttp.NewServer()
defer server.Close(){ .api }
func NewTLSServer() *ServerCreates and starts a new HTTPS test server with a self-signed certificate.
Returns: *Server - A started HTTPS server
Example:
server := ghttp.NewTLSServer()
defer server.Close()
// Use server.HTTPTestServer().Client() to get client with proper TLS config
client := server.HTTPTestServer().Client(){ .api }
func NewUnstartedServer() *ServerCreates a new test server but does not start it. You must call Start() manually.
Returns: *Server - An unstarted server
Example:
server := ghttp.NewUnstartedServer()
// Configure server...
server.Start()
defer server.Close(){ .api }
func (s *Server) Start()Starts an unstarted server. No-op if server is already started.
{ .api }
func (s *Server) Close()Closes the server and verifies all expected handlers were called. Fails the test if unexpected requests were received or expected handlers were not invoked.
{ .api }
func (s *Server) URL() stringReturns the full URL of the server including scheme and port.
Returns: Server URL (e.g., "http://127.0.0.1:54321")
Example:
resp, err := http.Get(server.URL() + "/api/users"){ .api }
func (s *Server) Addr() stringReturns the address (host:port) of the server.
Returns: Server address (e.g., "127.0.0.1:54321")
{ .api }
func (s *Server) HTTPTestServer() *httptest.ServerReturns the underlying *httptest.Server for advanced configuration.
Returns: The underlying httptest.Server
{ .api }
func (s *Server) AppendHandlers(handlers ...http.HandlerFunc)Appends one or more handlers to the server's handler queue. Handlers are invoked in order as requests arrive.
Parameters:
handlers - HTTP handler functions to add to the queueExample:
server.AppendHandlers(
ghttp.RespondWith(200, "first response"),
ghttp.RespondWith(200, "second response"),
){ .api }
func (s *Server) SetHandler(index int, handler http.HandlerFunc)Sets the handler at a specific index in the handler queue.
Parameters:
index - Zero-based index of handler to replacehandler - New handler function{ .api }
func (s *Server) GetHandler(index int) http.HandlerFuncReturns the handler at the specified index.
Parameters:
index - Zero-based index of handlerReturns: The handler at the specified index
{ .api }
func (s *Server) RouteToHandler(method string, path any, handler http.HandlerFunc)Routes requests matching the method and path to the specified handler. This bypasses the handler queue.
Parameters:
method - HTTP method (GET, POST, etc.)path - Path string or matcherhandler - Handler to invoke for matching requestsExample:
server.RouteToHandler("GET", "/health", ghttp.RespondWith(200, "OK"))
server.RouteToHandler("POST", "/api/users", ghttp.RespondWithJSON(201, user)){ .api }
func (s *Server) ReceivedRequests() []*http.RequestReturns a slice of all requests received by the server.
Returns: Slice of *http.Request
Example:
requests := server.ReceivedRequests()
Expect(requests).To(HaveLen(2))
Expect(requests[0].URL.Path).To(Equal("/api/users")){ .api }
func (s *Server) Reset()Clears all handlers and request history. Useful for resetting state between tests.
{ .api }
func (s *Server) SetUnhandledRequestCallback(callback func(http.ResponseWriter, *http.Request))Sets a callback function to handle requests that don't match any handler.
Parameters:
callback - Function to handle unmatched requestsExample:
server.SetUnhandledRequestCallback(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(404)
w.Write([]byte("Not Found"))
}){ .api }
func RespondWith(statusCode int, body any, optionalHeader ...http.Header) http.HandlerFuncCreates a handler that responds with a fixed status code and body.
Parameters:
statusCode - HTTP status code (e.g., 200, 404)body - Response body. Can be string, []byte, or any object (will be JSON-encoded)optionalHeader - Optional HTTP headers to include in responseReturns: http.HandlerFunc
Examples:
// Simple string response
ghttp.RespondWith(200, "OK")
// JSON response from object
ghttp.RespondWith(200, `{"name": "Alice"}`)
// With custom headers
ghttp.RespondWith(200, "OK", http.Header{
"Content-Type": []string{"text/plain"},
"X-Custom": []string{"value"},
}){ .api }
func RespondWithPtr(statusCode *int, body any, optionalHeader ...http.Header) http.HandlerFuncLike RespondWith, but takes a pointer to status code, allowing the status to be changed dynamically.
Parameters:
statusCode - Pointer to HTTP status codebody - Response bodyoptionalHeader - Optional headersReturns: http.HandlerFunc
Example:
code := 200
server.AppendHandlers(ghttp.RespondWithPtr(&code, "response"))
// Later, change the status code
code = 500{ .api }
func RespondWithJSON(statusCode int, body any, optionalHeader ...http.Header) http.HandlerFuncCreates a handler that responds with JSON-encoded body and appropriate Content-Type header.
Parameters:
statusCode - HTTP status codebody - Object to JSON-encode (must be JSON-serializable)optionalHeader - Optional headers (Content-Type will be set to application/json)Returns: http.HandlerFunc
Example:
user := map[string]string{
"name": "Alice",
"email": "alice@example.com",
}
server.AppendHandlers(ghttp.RespondWithJSON(200, user)){ .api }
func VerifyRequest(method string, path any, rawQuery ...any) http.HandlerFuncCreates a handler that verifies the request method, path, and optionally query string. Fails the test if verification fails.
Parameters:
method - Expected HTTP method (GET, POST, etc.)path - Expected path (string or matcher)rawQuery - Optional query string verification (string, matcher, or nil to skip)Returns: http.HandlerFunc
Examples:
// Verify method and path
ghttp.VerifyRequest("GET", "/api/users")
// Verify with query parameters
ghttp.VerifyRequest("GET", "/search", "q=golang&page=1")
// Use matcher for path
ghttp.VerifyRequest("GET", MatchRegexp(`/api/users/\d+`)){ .api }
func VerifyJSON(expectedJSON string) http.HandlerFuncCreates a handler that verifies the request body is semantically equivalent JSON.
Parameters:
expectedJSON - Expected JSON stringReturns: http.HandlerFunc
Example:
expectedJSON := `{
"name": "Alice",
"age": 30
}`
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/api/users"),
ghttp.VerifyJSON(expectedJSON),
ghttp.RespondWithJSON(201, user),
),
){ .api }
func VerifyBody(expectedBody []byte) http.HandlerFuncCreates a handler that verifies the request body exactly matches the expected bytes.
Parameters:
expectedBody - Expected body as byte sliceReturns: http.HandlerFunc
Example:
ghttp.VerifyBody([]byte("exact body content")){ .api }
func VerifyHeader(header http.Header) http.HandlerFuncCreates a handler that verifies the request contains the specified headers.
Parameters:
header - Expected headers (all must be present with exact values)Returns: http.HandlerFunc
Example:
expectedHeaders := http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{"Bearer token123"},
}
ghttp.VerifyHeader(expectedHeaders){ .api }
func VerifyHeaderKV(key string, values ...string) http.HandlerFuncCreates a handler that verifies the request contains a specific header key with the specified values.
Parameters:
key - Header namevalues - Expected header values (variadic)Returns: http.HandlerFunc
Example:
ghttp.VerifyHeaderKV("Content-Type", "application/json")
ghttp.VerifyHeaderKV("Accept", "application/json", "text/plain"){ .api }
func VerifyHost(host any) http.HandlerFuncCreates a handler that verifies the request Host header matches the expected value.
Parameters:
host - Expected host (string or matcher)Returns: http.HandlerFunc
Example:
ghttp.VerifyHost("api.example.com")
ghttp.VerifyHost(MatchRegexp(`.*\.example\.com`)){ .api }
func VerifyContentType(contentType string) http.HandlerFuncCreates a handler that verifies the Content-Type header.
Parameters:
contentType - Expected Content-Type valueReturns: http.HandlerFunc
Example:
ghttp.VerifyContentType("application/json"){ .api }
func VerifyMimeType(mimeType string) http.HandlerFuncCreates a handler that verifies the MIME type portion of the Content-Type header (ignoring charset and other parameters).
Parameters:
mimeType - Expected MIME typeReturns: http.HandlerFunc
Example:
ghttp.VerifyMimeType("application/json")
// Matches "application/json; charset=utf-8"{ .api }
func VerifyJSONRepresenting(object any) http.HandlerFuncCreates a handler that verifies the request body is JSON that marshals to match the provided object.
Parameters:
object - Object to compare againstReturns: http.HandlerFunc
Example:
expected := User{Name: "Alice", Age: 30}
ghttp.VerifyJSONRepresenting(expected){ .api }
func VerifyForm(values url.Values) http.HandlerFuncCreates a handler that verifies the request contains the specified form values.
Parameters:
values - Expected form valuesReturns: http.HandlerFunc
Example:
expected := url.Values{
"username": []string{"alice"},
"password": []string{"secret"},
}
ghttp.VerifyForm(expected){ .api }
func VerifyFormKV(key string, values ...string) http.HandlerFuncCreates a handler that verifies the request contains a specific form key with the specified values.
Parameters:
key - Form field namevalues - Expected form field values (variadic)Returns: http.HandlerFunc
Example:
ghttp.VerifyFormKV("username", "alice")
ghttp.VerifyFormKV("roles", "admin", "user"){ .api }
func VerifyBasicAuth(username string, password string) http.HandlerFuncCreates a handler that verifies HTTP Basic Authentication credentials.
Parameters:
username - Expected usernamepassword - Expected passwordReturns: http.HandlerFunc
Example:
ghttp.VerifyBasicAuth("admin", "secret123"){ .api }
func CombineHandlers(handlers ...http.HandlerFunc) http.HandlerFuncCombines multiple handlers into a single handler. All handlers are invoked in sequence.
Parameters:
handlers - Handlers to combineReturns: http.HandlerFunc that executes all handlers
Example:
handler := ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/api/users"),
ghttp.VerifyJSON(`{"name": "Alice"}`),
ghttp.VerifyContentType("application/json"),
ghttp.RespondWithJSON(201, map[string]string{
"id": "123",
"name": "Alice",
}),
)
server.AppendHandlers(handler)package client_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/ghttp"
)
var _ = Describe("HTTP Client", func() {
var server *ghttp.Server
BeforeEach(func() {
server = ghttp.NewServer()
})
AfterEach(func() {
server.Close()
})
It("should fetch user data", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/users/123"),
ghttp.RespondWithJSON(200, map[string]interface{}{
"id": "123",
"name": "Alice",
}),
),
)
client := NewClient(server.URL())
user, err := client.GetUser("123")
Expect(err).NotTo(HaveOccurred())
Expect(user.Name).To(Equal("Alice"))
Expect(server.ReceivedRequests()).To(HaveLen(1))
})
})It("should create a user", func() {
expectedRequest := `{
"name": "Bob",
"email": "bob@example.com"
}`
expectedResponse := map[string]interface{}{
"id": "456",
"name": "Bob",
"email": "bob@example.com",
}
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/api/users"),
ghttp.VerifyJSON(expectedRequest),
ghttp.VerifyContentType("application/json"),
ghttp.RespondWithJSON(201, expectedResponse),
),
)
client := NewClient(server.URL())
user, err := client.CreateUser("Bob", "bob@example.com")
Expect(err).NotTo(HaveOccurred())
Expect(user.ID).To(Equal("456"))
})It("should authenticate with bearer token", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/protected"),
ghttp.VerifyHeaderKV("Authorization", "Bearer secret-token"),
ghttp.RespondWith(200, "Protected resource"),
),
)
client := NewClient(server.URL())
client.SetToken("secret-token")
data, err := client.GetProtectedResource()
Expect(err).NotTo(HaveOccurred())
Expect(data).To(Equal("Protected resource"))
})
It("should authenticate with basic auth", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/admin"),
ghttp.VerifyBasicAuth("admin", "password123"),
ghttp.RespondWith(200, "Admin data"),
),
)
client := NewClient(server.URL())
client.SetBasicAuth("admin", "password123")
data, err := client.GetAdminData()
Expect(err).NotTo(HaveOccurred())
})It("should handle multiple sequential requests", func() {
server.AppendHandlers(
ghttp.RespondWithJSON(200, map[string]string{"page": "1"}),
ghttp.RespondWithJSON(200, map[string]string{"page": "2"}),
ghttp.RespondWithJSON(200, map[string]string{"page": "3"}),
)
client := NewClient(server.URL())
page1, _ := client.GetPage(1)
page2, _ := client.GetPage(2)
page3, _ := client.GetPage(3)
Expect(page1["page"]).To(Equal("1"))
Expect(page2["page"]).To(Equal("2"))
Expect(page3["page"]).To(Equal("3"))
Expect(server.ReceivedRequests()).To(HaveLen(3))
})It("should verify query parameters", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/search", "q=golang&limit=10"),
ghttp.RespondWithJSON(200, []string{"result1", "result2"}),
),
)
client := NewClient(server.URL())
results, err := client.Search("golang", 10)
Expect(err).NotTo(HaveOccurred())
Expect(results).To(HaveLen(2))
})It("should handle 404 errors", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/users/999"),
ghttp.RespondWith(404, `{"error": "User not found"}`),
),
)
client := NewClient(server.URL())
_, err := client.GetUser("999")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))
})
It("should handle 500 errors", func() {
server.AppendHandlers(
ghttp.RespondWith(500, "Internal Server Error"),
)
client := NewClient(server.URL())
_, err := client.GetUser("123")
Expect(err).To(HaveOccurred())
})BeforeEach(func() {
server = ghttp.NewServer()
// Set up routes that persist across requests
server.RouteToHandler("GET", "/health",
ghttp.RespondWith(200, "OK"),
)
server.RouteToHandler("GET", "/api/users",
ghttp.RespondWithJSON(200, []map[string]string{
{"id": "1", "name": "Alice"},
{"id": "2", "name": "Bob"},
}),
)
})
It("can call health endpoint multiple times", func() {
client := NewClient(server.URL())
// Can call multiple times without adding more handlers
Expect(client.CheckHealth()).To(BeTrue())
Expect(client.CheckHealth()).To(BeTrue())
Expect(client.CheckHealth()).To(BeTrue())
})var _ = Describe("HTTPS Client", func() {
var server *ghttp.Server
BeforeEach(func() {
server = ghttp.NewTLSServer()
})
AfterEach(func() {
server.Close()
})
It("should work over HTTPS", func() {
server.AppendHandlers(
ghttp.RespondWith(200, "secure response"),
)
// Use the test server's client which trusts the self-signed cert
client := server.HTTPTestServer().Client()
resp, err := client.Get(server.URL() + "/api/data")
Expect(err).NotTo(HaveOccurred())
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
Expect(string(body)).To(Equal("secure response"))
})
})It("should include custom headers", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/data"),
ghttp.VerifyHeaderKV("X-Request-ID", "12345"),
ghttp.VerifyHeaderKV("X-Client-Version", "1.0.0"),
ghttp.RespondWith(200, "data", http.Header{
"X-Response-ID": []string{"67890"},
}),
),
)
client := NewClient(server.URL())
client.SetHeader("X-Request-ID", "12345")
client.SetHeader("X-Client-Version", "1.0.0")
resp, err := client.GetData()
Expect(err).NotTo(HaveOccurred())
Expect(resp.Header.Get("X-Response-ID")).To(Equal("67890"))
})It("should verify request body with matchers", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/api/logs"),
func(w http.ResponseWriter, req *http.Request) {
body, _ := io.ReadAll(req.Body)
Expect(string(body)).To(ContainSubstring("error"))
Expect(string(body)).To(ContainSubstring("timestamp"))
},
ghttp.RespondWith(201, "Log created"),
),
)
client := NewClient(server.URL())
err := client.SendLog("error", time.Now())
Expect(err).NotTo(HaveOccurred())
})It("should timeout on slow responses", func() {
server.AppendHandlers(
func(w http.ResponseWriter, req *http.Request) {
time.Sleep(5 * time.Second)
w.WriteHeader(200)
},
)
client := NewClient(server.URL())
client.SetTimeout(1 * time.Second)
_, err := client.GetData()
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("timeout"))
})It("should track all requests", func() {
server.AppendHandlers(
ghttp.RespondWith(200, "response1"),
ghttp.RespondWith(200, "response2"),
)
client := NewClient(server.URL())
client.Get("/endpoint1")
client.Get("/endpoint2")
requests := server.ReceivedRequests()
Expect(requests).To(HaveLen(2))
Expect(requests[0].URL.Path).To(Equal("/endpoint1"))
Expect(requests[1].URL.Path).To(Equal("/endpoint2"))
})It("should allow dynamic response modification", func() {
statusCode := 200
server.AppendHandlers(
ghttp.RespondWithPtr(&statusCode, "response"),
ghttp.RespondWithPtr(&statusCode, "response"),
)
client := NewClient(server.URL())
// First request succeeds
_, err := client.Get("/data")
Expect(err).NotTo(HaveOccurred())
// Change status code
statusCode = 503
// Second request fails
_, err = client.Get("/data")
Expect(err).To(HaveOccurred())
})Always Close Servers: Use defer server.Close() in AfterEach to ensure proper cleanup and verification.
Verify Requests: Use verification handlers to ensure your client sends correct requests.
Test Both Success and Failure: Simulate error responses to test error handling.
Use CombineHandlers: Combine verification and response handlers for comprehensive testing.
Check Request History: Use ReceivedRequests() to verify the number and order of requests.
Use RouteToHandler for Repeated Endpoints: For endpoints called multiple times, use routing instead of queue-based handlers.
Test HTTPS When Needed: Use NewTLSServer() and the test server's client for HTTPS testing.
Reset Between Tests: Call server.Reset() if reusing a server across multiple test cases.
var _ = Describe("API Client", func() {
var (
server *ghttp.Server
client *APIClient
)
BeforeEach(func() {
server = ghttp.NewServer()
client = NewAPIClient(server.URL())
})
AfterEach(func() {
server.Close()
})
Describe("User Management", func() {
It("lists users", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/users"),
ghttp.RespondWithJSON(200, []User{{ID: "1", Name: "Alice"}}),
),
)
users, err := client.ListUsers()
Expect(err).NotTo(HaveOccurred())
Expect(users).To(HaveLen(1))
})
It("creates a user", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/api/users"),
ghttp.VerifyJSON(`{"name":"Bob"}`),
ghttp.RespondWithJSON(201, User{ID: "2", Name: "Bob"}),
),
)
user, err := client.CreateUser("Bob")
Expect(err).NotTo(HaveOccurred())
Expect(user.ID).To(Equal("2"))
})
})
})This comprehensive approach enables thorough testing of HTTP clients with minimal boilerplate and maximum flexibility.