httpexpect is an open-source Go project with 2.7k GitHub stars. It is associated with assertions, http, json, rest. The repository has seen commits within the last year.
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
URL path construction, with simple string interpolation provided by go-interpol package.
URL query parameters (encoding using go-querystring package).
Headers, cookies, payload: JSON, urlencoded or multipart forms (encoding using form package), plain text.
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.
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.
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
// 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)
URL construction
// 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"
Headers
// 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())
Cookies
// 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))
Regular expressions
// 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 := 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)
Request transformers
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)
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()
// 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)
})
Custom config
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),
},
})
Use HTTP handler directly
// 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(),
},
})
Per-request client or handler
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)
WebSocket dialer
// 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),
})
Session support
// 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,
},
})
TLS support
// 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{},
},
},
})
// 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)
Choosing failure reporter
// 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),
})
Assigning names to requests
// 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()
Assigning aliases to values
// 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()
Printing requests and responses
// 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),
},
})
// 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{},
})
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)