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.
- 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.
- 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. - Dumping requests and responses in various formats, using
httputil,http2curl, or simple compact logger.
- Tests can communicate with server via real HTTP client or invoke
net/httporfasthttphandler directly. - Custom HTTP client, logger, printer, and failure reporter may be provided by user.
- Custom HTTP request factory may be provided, e.g. from the Google App Engine testing.
Stable branches are available on gopkg.in and will not introduce backward-incompatible changes.
Current stable branch is v2:
import "gopkg.in/gavv/httpexpect.v2"Development is done in master branch on github:
import "github.com/gavv/httpexpect"When the master is merged into a stable branch, a new version tag is assigned to the branch head. The versions are selected according to the semantic versioning scheme.
Documentation is available on GoDoc. It contains an overview and reference.
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 made with
fasthttppackage. Tests invoke thefasthttp.RequestHandlerdirectly. -
Testing a WebSocket server based on
gorilla/websocket. Tests invoke thehttp.Handlerorfasthttp.RequestHandlerdirectly. -
Testing a server running under the Google App Engine.
package example
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gavv/httpexpect"
)
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.New(t, server.URL)
// is it working?
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().Empty()
}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").ValueEqual("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().Elements("green", "red")
obj.Value("colors").Array().Element(0).String().Equal("green")
obj.Value("colors").Array().Element(1).String().Equal("red")
obj.Value("colors").Array().First().String().Equal("green")
obj.Value("colors").Array().Last().String().Equal("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().False()
}// 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").DateTime().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().Equal(sessionID)
c.Domain().Equal("example.com")
c.Path().Equal("/")
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.Index(0).Equal("http://example.com/users/john")
m.Index(1).Equal("example.com")
m.Index(2).Equal("john")
m.Name("host").Equal("example.com")
m.Name("user").Equal("john")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().Equal("some response")
ws.CloseWithText("bye").
Expect().
CloseMessage().NoContent()e := httpexpect.New(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.New(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.WithConfig(httpexpect.Config{
// prepend this url to all requests
BaseURL: "http://example.com",
// use http.Client with a cookie jar and timeout
Client: &http.Client{
Jar: httpexpect.NewJar(),
Timeout: time.Second * 30,
},
// use fatal failures
Reporter: httpexpect.NewRequireReporter(t),
// use verbose logging
Printers: []httpexpect.Printer{
httpexpect.NewCurlPrinter(t),
httpexpect.NewDebugPrinter(t, true),
},
})// cookie jar is used to store cookies from server
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: httpexpect.NewJar(), // used by default if Client is nil
},
})
// cookies are disabled
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: nil,
},
})// invoke http.Handler directly using httpexpect.Binder
var handler http.Handler = MyHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewBinder(handler),
Jar: httpexpect.NewJar(),
},
})
// invoke fasthttp.RequestHandler directly using httpexpect.FastBinder
var handler fasthttp.RequestHandler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewFastBinder(handler),
Jar: httpexpect.NewJar(),
},
})e := httpexpect.New(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)Feel free to report bugs, suggest improvements, and send pull requests! Please add documentation and tests for new features.
Update dependencies, build code, and run tests and linters:
$ make
Format code:
$ make fmt