
httpexpect

Concise, declarative, and easy to use end-to-end HTTP and REST API testing for Go (golang).
Basically, httpexpect is a set of chainable builders for HTTP requests and assertions for HTTP responses and payload, on top of net/http and several utility packages.
Workflow:
- Incrementally build HTTP requests.
- Inspect HTTP responses.
- Inspect response payload recursively.
Features
Request builder
Response assertions
- Response status, predefined status ranges.
- Headers, cookies, payload: JSON, JSONP, forms, text.
- Round-trip time.
- Custom reusable response matchers.
Payload assertions
- Type-specific assertions, supported types: object, array, string, number, boolean, null, datetime, duration, cookie.
- Regular expressions.
- Simple JSON queries (using subset of JSONPath), provided by
jsonpath
package.
- JSON Schema validation, provided by
gojsonschema
package.
WebSocket support (thanks to @tyranron)
- Upgrade an HTTP connection to a WebSocket connection (we use
gorilla/websocket
internally).
- Interact with the WebSocket server.
- Inspect WebSocket connection parameters and WebSocket messages.
Pretty printing
- Verbose error messages.
- JSON diff is produced on failure using
gojsondiff
package.
- Failures are reported using
testify
(assert
or require
package) or standard testing
package.
- JSON values are pretty-printed using
encoding/json
, Go values are pretty-printed using litter
.
- Dumping requests and responses in various formats, using
httputil
, http2curl
, or simple compact logger.
- Printing stacktrace on failure in verbose or compact format.
- Color support using
fatih/color
.
Tuning
- Tests can communicate with server via real HTTP client or invoke
net/http
or fasthttp
handler directly.
- User can provide custom HTTP client, WebSocket dialer, HTTP request factory (e.g. from the Google App Engine testing).
- User can configure redirect and retry policies and timeouts.
- User can configure formatting options (what parts to display, how to format numbers, etc.) or provide custom templates based on
text/template
engine.
- Custom handlers may be provided for logging, printing requests and responses, handling succeeded and failed assertions.
Versioning
The versions are selected according to the semantic versioning scheme. Every new major version gets its own stable branch with a backwards compatibility promise. Releases are tagged from stable branches.
Changelog file can be found here: changelog.
The current stable branch is v2
:
import "github.com/gavv/httpexpect/v2"
Documentation
Documentation is available on pkg.go.dev. It contains an overview and reference.
Community forum and Q&A board is right on GitHub in discussions tab.
For more interactive discussion, you can join discord chat.
Contributing
Feel free to report bugs, suggest improvements, and send pull requests! Please add documentation and tests for new features.
This project highly depends on contributors. Thank you all for your amazing work!
If you would like to submit code, see HACKING.md.
Donating
If you would like to support my open-source work, you can do it here:
Thanks!
Examples
See _examples
directory for complete standalone examples.
-
fruits_test.go
Testing a simple CRUD server made with bare net/http
.
-
iris_test.go
Testing a server made with iris
framework. Example includes JSON queries and validation, URL and form parameters, basic auth, sessions, and streaming. Tests invoke the http.Handler
directly.
-
echo_test.go
Testing a server with JWT authentication made with echo
framework. Tests use either HTTP client or invoke the http.Handler
directly.
-
gin_test.go
Testing a server utilizing the gin
web framework. Tests invoke the http.Handler
directly.
-
fasthttp_test.go
Testing a server made with fasthttp
package. Tests invoke the fasthttp.RequestHandler
directly.
-
websocket_test.go
Testing a WebSocket server based on gorilla/websocket
. Tests invoke the http.Handler
or fasthttp.RequestHandler
directly.
-
tls_test.go
Testing a TLS server made with net/http
and crypto/tls
-
oauth2_test.go
Testing a OAuth2 server with oauth2
.
-
gae_test.go
Testing a server running under the Google App Engine.
-
formatter_test.go
Testing with custom formatter for assertion messages.
Quick start
Hello, world!
package example
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gavv/httpexpect/v2"
)
func TestFruits(t *testing.T) {
handler := FruitsHandler()
server := httptest.NewServer(handler)
defer server.Close()
e := httpexpect.Default(t, server.URL)
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().IsEmpty()
}
JSON
orange := map[string]interface{}{
"weight": 100,
}
e.PUT("/fruits/orange").WithJSON(orange).
Expect().
Status(http.StatusNoContent).NoContent()
e.GET("/fruits/orange").
Expect().
Status(http.StatusOK).
JSON().Object().ContainsKey("weight").HasValue("weight", 100)
apple := map[string]interface{}{
"colors": []interface{}{"green", "red"},
"weight": 200,
}
e.PUT("/fruits/apple").WithJSON(apple).
Expect().
Status(http.StatusNoContent).NoContent()
obj := e.GET("/fruits/apple").
Expect().
Status(http.StatusOK).JSON().Object()
obj.Keys().ContainsOnly("colors", "weight")
obj.Value("colors").Array().ConsistsOf("green", "red")
obj.Value("colors").Array().Value(0).String().IsEqual("green")
obj.Value("colors").Array().Value(1).String().IsEqual("red")
obj.Value("colors").Array().First().String().IsEqual("green")
obj.Value("colors").Array().Last().String().IsEqual("red")
JSON Schema and JSON Path
schema := `{
"type": "array",
"items": {
"type": "object",
"properties": {
...
"private": {
"type": "boolean"
}
}
}
}`
repos := e.GET("/repos/octocat").
Expect().
Status(http.StatusOK).JSON()
repos.Schema(schema)
for _, private := range repos.Path("$..private").Array().Iter() {
private.Boolean().IsFalse()
}
JSON decoding
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Gender string `json:"gender"`
}
var user User
e.GET("/user").
Expect().
Status(http.StatusOK).
JSON().
Decode(&user)
if user.Name != "octocat" {
t.Fail()
}
Forms
e.POST("/form").WithForm(structOrMap).
Expect().
Status(http.StatusOK)
e.POST("/form").WithFormField("foo", "hello").WithFormField("bar", 123).
Expect().
Status(http.StatusOK)
e.POST("/form").WithMultipart().
WithFile("avatar", "./john.png").WithFormField("username", "john").
Expect().
Status(http.StatusOK)
URL construction
e.GET("/repos/{user}/{repo}", "octocat", "hello-world").
Expect().
Status(http.StatusOK)
e.GET("/repos/{user}/{repo}").
WithPath("user", "octocat").WithPath("repo", "hello-world").
Expect().
Status(http.StatusOK)
e.GET("/repos/{user}", "octocat").WithQuery("sort", "asc").
Expect().
Status(http.StatusOK)
e.POST("/users/john").WithHeader("If-Match", etag).WithJSON(john).
Expect().
Status(http.StatusOK)
e.GET("/users/john").
Expect().
Status(http.StatusOK).Header("ETag").NotEmpty()
t := time.Now()
e.GET("/users/john").
Expect().
Status(http.StatusOK).Header("Date").AsDateTime().InRange(t, time.Now())
Cookies
t := time.Now()
e.POST("/users/john").WithCookie("session", sessionID).WithJSON(john).
Expect().
Status(http.StatusOK)
c := e.GET("/users/john").
Expect().
Status(http.StatusOK).Cookie("session")
c.Value().IsEqual(sessionID)
c.Domain().IsEqual("example.com")
c.Path().IsEqual("/")
c.Expires().InRange(t, t.Add(time.Hour * 24))
Regular expressions
e.GET("/users/john").
Expect().
Header("Location").
Match("http://(.+)/users/(.+)").Values("example.com", "john")
m := e.GET("/users/john").
Expect().
Header("Location").Match("http://(?P<host>.+)/users/(?P<user>.+)")
m.Submatch(0).IsEqual("http://example.com/users/john")
m.Submatch(1).IsEqual("example.com")
m.Submatch(2).IsEqual("john")
m.NamedSubmatch("host").IsEqual("example.com")
m.NamedSubmatch("user").IsEqual("john")
Redirection support
e.POST("/path").
WithRedirectPolicy(httpexpect.FollowAllRedirects).
WithMaxRedirects(5).
Expect().
Status(http.StatusOK)
e.POST("/path").
WithRedirectPolicy(httpexpect.DontFollowRedirects).
Expect().
Status(http.StatusPermanentRedirect)
Retry support
e.POST("/path").
WithMaxRetries(5).
Expect().
Status(http.StatusOK)
e.POST("/path").
WithMaxRetries(5).
WithRetryPolicy(httpexpect.RetryAllErrors).
Expect().
Status(http.StatusOK)
e.POST("/path").
WithMaxRetries(5).
WithRetryDelay(time.Second, time.Minute).
Expect().
Status(http.StatusOK)
e.POST("/path").
WithMaxRetries(5).
WithRetryPolicyFunc(func(resp *http.Response, err error) bool {
return resp.StatusCode == http.StatusTeapot
}).
Expect().
Status(http.StatusOK)
Subdomains and per-request URL
e.GET("/path").WithURL("http://example.com").
Expect().
Status(http.StatusOK)
e.GET("/path").WithURL("http://subdomain.example.com").
Expect().
Status(http.StatusOK)
WebSocket support
ws := e.GET("/mysocket").WithWebsocketUpgrade().
Expect().
Status(http.StatusSwitchingProtocols).
Websocket()
defer ws.Disconnect()
ws.WriteText("some request").
Expect().
TextMessage().Body().IsEqual("some response")
ws.CloseWithText("bye").
Expect().
CloseMessage().NoContent()
Reusable builders
e := httpexpect.Default(t, "http://example.com")
r := e.POST("/login").WithForm(Login{"ford", "betelgeuse7"}).
Expect().
Status(http.StatusOK).JSON().Object()
token := r.Value("token").String().Raw()
auth := e.Builder(func (req *httpexpect.Request) {
req.WithHeader("Authorization", "Bearer "+token)
})
auth.GET("/restricted").
Expect().
Status(http.StatusOK)
e.GET("/restricted").
Expect().
Status(http.StatusUnauthorized)
Reusable matchers
e := httpexpect.Default(t, "http://example.com")
m := e.Matcher(func (resp *httpexpect.Response) {
resp.Header("API-Version").NotEmpty()
})
m.GET("/some-path").
Expect().
Status(http.StatusOK)
m.GET("/bad-path").
Expect().
Status(http.StatusNotFound)
Request transformers
e := httpexpect.Default(t, "http://example.com")
myTranform := func(r* http.Request) {
}
e.POST("/some-path").
WithTransformer(myTranform).
Expect().
Status(http.StatusOK)
myBuilder := e.Builder(func (req *httpexpect.Request) {
req.WithTransformer(myTranform)
})
myBuilder.POST("/some-path").
Expect().
Status(http.StatusOK)
Shared environment
e := httpexpect.Default(t, "http://example.com")
t.Run("/users", func(t *testing.T) {
obj := e.GET("/users").
Expect().
Status(http.StatusOK).JSON().Object()
userID := obj.Path("$.users[1].id").String().Raw()
e.Env().Put("user1.id", userID)
})
t.Run("/user/{userId}", func(t *testing.T) {
userID := e.Env().GetString("user1.id")
e.GET("/user/{userId}").
WithPath("userId", userID)
Expect().
Status(http.StatusOK)
})
Custom config
e := httpexpect.WithConfig(httpexpect.Config{
TestName: t.Name(),
BaseURL: "http://example.com",
Client: &http.Client{
Jar: httpexpect.NewCookieJar(),
Timeout: time.Second * 30,
},
Reporter: httpexpect.NewRequireReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewDebugPrinter(t, true),
},
})
Use HTTP handler directly
var handler http.Handler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewBinder(handler),
Jar: httpexpect.NewCookieJar(),
},
})
var handler fasthttp.RequestHandler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewFastBinder(handler),
Jar: httpexpect.NewCookieJar(),
},
})
Per-request client or handler
e := httpexpect.Default(t, server.URL)
client := &http.Client{
Transport: &http.Transport{
DisableCompression: true,
},
}
e.GET("/path").WithClient(client).
Expect().
Status(http.StatusOK)
e.GET("/path").WithHandler(handler).
Expect().
Status(http.StatusOK)
WebSocket dialer
var handler http.Handler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
WebsocketDialer: httpexpect.NewWebsocketDialer(handler),
})
var handler fasthttp.RequestHandler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
WebsocketDialer: httpexpect.NewFastWebsocketDialer(handler),
})
Session support
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: httpexpect.NewCookieJar(),
},
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: nil,
},
})
TLS support
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
},
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: &httpexpect.Binder{
Handler: myHandler,
TLS: &tls.ConnectionState{},
},
},
})
Proxy support
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL("http://proxy.example.com"),
},
},
})
Global timeout/cancellation
handler := FruitsHandler()
server := httptest.NewServer(handler)
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
e := WithConfig(Config{
BaseURL: server.URL,
Reporter: httpexpect.NewAssertReporter(t),
Context: ctx,
})
go func() {
time.Sleep(time.Duration(5)*time.Second)
cancel()
}()
e.GET("/fruits").
Expect().
Status(http.StatusOK)
Per-request timeout/cancellation
e.GET("/fruits").
WithContext(context.TODO()).
Expect().
Status(http.StatusOK)
e.GET("/fruits").
WithTimeout(time.Duration(5)*time.Second).
Expect().
Status(http.StatusOK)
e.POST("/fruits").
WithMaxRetries(5).
WithTimeout(time.Duration(10)*time.Second).
Expect().
Status(http.StatusOK)
Choosing failure reporter
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewRequireReporter(t),
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: t,
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewFatalReporter(t),
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewPanicReporter(t),
})
Assigning names to requests
e.GET("/fruits").
WithName("Get Fruits")
Expect().
Status(http.StatusOK).JSON().Array().IsEmpty()
Assigning aliases to values
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().IsEmpty()
fruits := e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().Alias("fruits")
fruits.IsEmpty()
Printing requests and responses
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewCompactPrinter(t),
},
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewCurlPrinter(t),
},
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewDebugPrinter(t, true),
},
})
Customize failure formatting
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Formatter: &httpexpect.DefaultFormatter{
DisablePaths: true,
DisableDiffs: true,
FloatFormat: httpexpect.FloatFormatScientific,
ColorMode: httpexpect.ColorModeNever,
LineWidth: 80,
},
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Formatter: &httpexpect.DefaultFormatter{
SuccessTemplate: "...",
FailureTemplate: "...",
TemplateFuncs: template.FuncMap{ ... },
},
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Formatter: &MyFormatter{},
})
Customize assertion handling
e := httpexpect.WithConfig(httpexpect.Config{
AssertionHandler: &httpexpect.DefaultAssertionHandler{
Formatter: &httpexpect.DefaultFormatter{},
Reporter: httpexpect.NewAssertReporter(t),
Logger: t,
},
})
e := httpexpect.WithConfig(httpexpect.Config{
AssertionHandler: &MyAssertionHandler{},
})
Environment variables
The following environment variables are checked when ColorModeAuto
is used:
FORCE_COLOR
- if set to a positive integers, colors are enabled
NO_COLOR
- if set to non-empty string, colors are disabled (see also)
TERM
- if starts with dumb
, colors are disabled
Similar packages
Authors
List of contributors can be found here.
If your name is missing or you want to change its appearance, feel free to submit PR!
License
MIT