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.
- URL path construction, with simple string interpolation provided by
go-interpolpackage. - URL query parameters (encoding using
go-querystringpackage). - Headers, cookies, payload: JSON, urlencoded or multipart forms (encoding using
formpackage), plain text. - Custom reusable request builders and request transformers.
- Response status, predefined status ranges.
- Headers, cookies, payload: JSON, JSONP, forms, text.
- Round-trip time.
- Custom reusable response matchers.
- 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
jsonpathpackage. - JSON Schema validation, provided by
gojsonschemapackage.
WebSocket support (thanks to @tyranron)
- Upgrade an HTTP connection to a WebSocket connection (we use
gorilla/websocketinternally). - Interact with the WebSocket server.
- Inspect WebSocket connection parameters and WebSocket messages.
- Verbose error messages.
- JSON diff is produced on failure using
gojsondiffpackage. - Failures are reported using
testify(assertorrequirepackage) or standardtestingpackage. - JSON values are pretty-printed using
encoding/json, Go values are pretty-printed usinglitter. - 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.
- Tests can communicate with server via real HTTP client or invoke
net/httporfasthttphandler 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/templateengine. - Custom handlers may be provided for logging, printing requests and responses, handling succeeded and failed assertions.
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 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.
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.
If you would like to support my open-source work, you can do it here:
Thanks!
See _examples directory for complete standalone examples.
-
Testing a simple CRUD server made with bare
net/http. -
Testing a server made with
irisframework. Example includes JSON queries and validation, URL and form parameters, basic auth, sessions, and streaming. Tests invoke thehttp.Handlerdirectly. -
Testing a server with JWT authentication made with
echoframework. Tests use either HTTP client or invoke thehttp.Handlerdirectly. -
Testing a server utilizing the
ginweb framework. Tests invoke thehttp.Handlerdirectly. -
Testing a server made with
fasthttppackage. Tests invoke thefasthttp.RequestHandlerdirectly. -
Testing a WebSocket server based on
gorilla/websocket. Tests invoke thehttp.Handlerorfasthttp.RequestHandlerdirectly. -
Testing a TLS server made with
net/httpandcrypto/tls -
Testing a OAuth2 server with
oauth2. -
Testing a server running under the Google App Engine.
-
Testing with custom formatter for assertion messages.
package example
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gavv/httpexpect/v2"
)
func TestFruits(t *testing.T) {
// create http.Handler
handler := FruitsHandler()
// run server using httptest
server := httptest.NewServer(handler)
defer server.Close()
// create httpexpect instance
e := httpexpect.Default(t, server.URL)
// is it working?
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().IsEmpty()
}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")schema := `{
"type": "array",
"items": {
"type": "object",
"properties": {
...
"private": {
"type": "boolean"
}
}
}
}`
repos := e.GET("/repos/octocat").
Expect().
Status(http.StatusOK).JSON()
// validate JSON schema
repos.Schema(schema)
// run JSONPath query and iterate results
for _, private := range repos.Path("$..private").Array().Iter() {
private.Boolean().IsFalse()
}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()
}// post form encoded from struct or map
e.POST("/form").WithForm(structOrMap).
Expect().
Status(http.StatusOK)
// set individual fields
e.POST("/form").WithFormField("foo", "hello").WithFormField("bar", 123).
Expect().
Status(http.StatusOK)
// multipart form
e.POST("/form").WithMultipart().
WithFile("avatar", "./john.png").WithFormField("username", "john").
Expect().
Status(http.StatusOK)// construct path using ordered parameters
e.GET("/repos/{user}/{repo}", "octocat", "hello-world").
Expect().
Status(http.StatusOK)
// construct path using named parameters
e.GET("/repos/{user}/{repo}").
WithPath("user", "octocat").WithPath("repo", "hello-world").
Expect().
Status(http.StatusOK)
// set query parameters
e.GET("/repos/{user}", "octocat").WithQuery("sort", "asc").
Expect().
Status(http.StatusOK) // "/repos/octocat?sort=asc"// set If-Match
e.POST("/users/john").WithHeader("If-Match", etag).WithJSON(john).
Expect().
Status(http.StatusOK)
// check ETag
e.GET("/users/john").
Expect().
Status(http.StatusOK).Header("ETag").NotEmpty()
// check Date
t := time.Now()
e.GET("/users/john").
Expect().
Status(http.StatusOK).Header("Date").AsDateTime().InRange(t, time.Now())// set cookie
t := time.Now()
e.POST("/users/john").WithCookie("session", sessionID).WithJSON(john).
Expect().
Status(http.StatusOK)
// check cookies
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))// simple match
e.GET("/users/john").
Expect().
Header("Location").
Match("http://(.+)/users/(.+)").Values("example.com", "john")
// check capture groups by index or name
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")e.POST("/path").
WithRedirectPolicy(httpexpect.FollowAllRedirects).
WithMaxRedirects(5).
Expect().
Status(http.StatusOK)
e.POST("/path").
WithRedirectPolicy(httpexpect.DontFollowRedirects).
Expect().
Status(http.StatusPermanentRedirect)// default retry policy
e.POST("/path").
WithMaxRetries(5).
Expect().
Status(http.StatusOK)
// custom built-in retry policy
e.POST("/path").
WithMaxRetries(5).
WithRetryPolicy(httpexpect.RetryAllErrors).
Expect().
Status(http.StatusOK)
// custom retry delays
e.POST("/path").
WithMaxRetries(5).
WithRetryDelay(time.Second, time.Minute).
Expect().
Status(http.StatusOK)
// custom user-defined retry policy
e.POST("/path").
WithMaxRetries(5).
WithRetryPolicyFunc(func(resp *http.Response, err error) bool {
return resp.StatusCode == http.StatusTeapot
}).
Expect().
Status(http.StatusOK)e.GET("/path").WithURL("http://example.com").
Expect().
Status(http.StatusOK)
e.GET("/path").WithURL("http://subdomain.example.com").
Expect().
Status(http.StatusOK)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()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)e := httpexpect.Default(t, "http://example.com")
// every response should have this header
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)e := httpexpect.Default(t, "http://example.com")
myTranform := func(r* http.Request) {
// modify the underlying http.Request
}
// apply transformer to a single request
e.POST("/some-path").
WithTransformer(myTranform).
Expect().
Status(http.StatusOK)
// create a builder that applies transfromer to every request
myBuilder := e.Builder(func (req *httpexpect.Request) {
req.WithTransformer(myTranform)
})
myBuilder.POST("/some-path").
Expect().
Status(http.StatusOK)e := httpexpect.Default(t, "http://example.com")
t.Run("/users", func(t *testing.T) {
obj := e.GET("/users").
Expect().
Status(http.StatusOK).JSON().Object()
// store user id for next tests
userID := obj.Path("$.users[1].id").String().Raw()
e.Env().Put("user1.id", userID)
})
t.Run("/user/{userId}", func(t *testing.T) {
// read user id from previous tests
userID := e.Env().GetString("user1.id")
e.GET("/user/{userId}").
WithPath("userId", userID)
Expect().
Status(http.StatusOK)
})e := httpexpect.WithConfig(httpexpect.Config{
// include test name in failures (optional)
TestName: t.Name(),
// prepend this url to all requests
BaseURL: "http://example.com",
// use http.Client with a cookie jar and timeout
Client: &http.Client{
Jar: httpexpect.NewCookieJar(),
Timeout: time.Second * 30,
},
// use fatal failures
Reporter: httpexpect.NewRequireReporter(t),
// print all requests and responses
Printers: []httpexpect.Printer{
httpexpect.NewDebugPrinter(t, true),
},
})// invoke http.Handler directly using httpexpect.Binder
var handler http.Handler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
// prepend this url to all requests, required for cookies
// to be handled correctly
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewBinder(handler),
Jar: httpexpect.NewCookieJar(),
},
})
// invoke fasthttp.RequestHandler directly using httpexpect.FastBinder
var handler fasthttp.RequestHandler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
// prepend this url to all requests, required for cookies
// to be handled correctly
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewFastBinder(handler),
Jar: httpexpect.NewCookieJar(),
},
})e := httpexpect.Default(t, server.URL)
client := &http.Client{
Transport: &http.Transport{
DisableCompression: true,
},
}
// overwrite client
e.GET("/path").WithClient(client).
Expect().
Status(http.StatusOK)
// construct client that invokes a handler directly and overwrite client
e.GET("/path").WithHandler(handler).
Expect().
Status(http.StatusOK)// invoke http.Handler directly using websocket.Dialer
var handler http.Handler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
WebsocketDialer: httpexpect.NewWebsocketDialer(handler),
})
// invoke fasthttp.RequestHandler directly using websocket.Dialer
var handler fasthttp.RequestHandler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
WebsocketDialer: httpexpect.NewFastWebsocketDialer(handler),
})// cookie jar is used to store cookies from server
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: httpexpect.NewCookieJar(), // used by default if Client is nil
},
})
// cookies are disabled
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: nil,
},
})// use TLS with http.Transport
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
// accept any certificate; for testing only!
InsecureSkipVerify: true,
},
},
},
})
// use TLS with http.Handler
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: &httpexpect.Binder{
Handler: myHandler,
TLS: &tls.ConnectionState{},
},
},
})e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL("http://proxy.example.com"),
},
},
})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 context
e.GET("/fruits").
WithContext(context.TODO()).
Expect().
Status(http.StatusOK)
// per-request timeout
e.GET("/fruits").
WithTimeout(time.Duration(5)*time.Second).
Expect().
Status(http.StatusOK)
// timeout combined with retries (timeout applies to each try)
e.POST("/fruits").
WithMaxRetries(5).
WithTimeout(time.Duration(10)*time.Second).
Expect().
Status(http.StatusOK)// default reporter, uses testify/assert
// failures don't terminate test immediately, but mark test as failed
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
})
// uses testify/require
// failures terminate test immediately
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewRequireReporter(t),
})
// if you're using bare testing.T without testify
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: t,
})
// if you're using bare testing.T and want failures to terminate test immediately
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewFatalReporter(t),
})
// if you want fatal failures triggered from other goroutines
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewPanicReporter(t),
})// when the tests fails, assertion message will mention request name:
// request name: Get Fruits
e.GET("/fruits").
WithName("Get Fruits")
Expect().
Status(http.StatusOK).JSON().Array().IsEmpty()// when the tests fails, assertion path in the failure message is:
// assertion: Request("GET").Expect().JSON().Array().IsEmpty()
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().IsEmpty()
// assign alias "fruits" to the Array variable
fruits := e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().Alias("fruits")
// assertion path in the failure message is now:
// assertion: fruits.IsEmpty()
fruits.IsEmpty()// print requests in short form, don't print responses
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewCompactPrinter(t),
},
})
// print requests as curl commands that can be inserted into terminal
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewCurlPrinter(t),
},
})
// print requests and responses in verbose form
// also print all incoming and outgoing websocket messages
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewDebugPrinter(t, true),
},
})// change formatting options
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Formatter: &httpexpect.DefaultFormatter{
DisablePaths: true,
DisableDiffs: true,
FloatFormat: httpexpect.FloatFormatScientific,
ColorMode: httpexpect.ColorModeNever,
LineWidth: 80,
},
})
// provide custom templates
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Formatter: &httpexpect.DefaultFormatter{
SuccessTemplate: "...",
FailureTemplate: "...",
TemplateFuncs: template.FuncMap{ ... },
},
})
// provide custom formatter
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Formatter: &MyFormatter{},
})// enable printing of succeeded assertions
e := httpexpect.WithConfig(httpexpect.Config{
AssertionHandler: &httpexpect.DefaultAssertionHandler{
Formatter: &httpexpect.DefaultFormatter{},
Reporter: httpexpect.NewAssertReporter(t),
Logger: t, // specify logger to enable printing of succeeded assertions
},
})
// provide custom assertion handler
// here you can implement custom handling of succeeded and failed assertions
// this may be useful for integrating httpexpect with other testing libs
// if desired, you can completely ignore builtin Formatter, Reporter, and Logger
e := httpexpect.WithConfig(httpexpect.Config{
AssertionHandler: &MyAssertionHandler{},
})The following environment variables are checked when ColorModeAuto is used:
FORCE_COLOR- if set to a positive integers, colors are enabledNO_COLOR- if set to non-empty string, colors are disabled (see also)TERM- if starts withdumb, colors are disabled
List of contributors can be found here.
If your name is missing or you want to change its appearance, feel free to submit PR!