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
- 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. - Create custom request builders that can be reused.
Response assertions
- Response status, predefined status ranges.
- Headers, cookies, payload: JSON, JSONP, forms, text.
- Round-trip time.
Payload assertions
- Type-specific assertions, supported types: object, array, string, number, boolean, null, datetime.
- Regular expressions.
- Simple JSON queries (using subset of JSONPath), provided by
jsonpath
package. - JSON Schema validation, provided by
gojsonschema
package.
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. - Dumping requests and responses in various formats, using
httputil
, http2curl
, or simple compact logger.
Tuning
- Tests can communicate with server via real HTTP client or invoke
net/http
or fasthttp
handler 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.
Status
Stable branches are available on gopkg.in
and will not introduce backward-incompatible changes.
Current stable branch is v1
:
import "gopkg.in/gavv/httpexpect.v1"
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
Documentation is available on GoDoc. It contains an overview and reference.
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.
-
fasthttp_test.go
Testing a server made with fasthttp
package. Tests invoke the fasthttp.RequestHandler
directly.
-
gae_test.go
Testing a server running under the Google App Engine.
Quick start
Hello, world!
package example
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gavv/httpexpect"
)
func TestFruits(t *testing.T) {
handler := FruitsHandler()
server := httptest.NewServer(handler)
defer server.Close()
e := httpexpect.New(t, server.URL)
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().Empty()
}
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").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")
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().False()
}
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").DateTime().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().Equal(sessionID)
c.Domain().Equal("example.com")
c.Path().Equal("/")
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.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")
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)
Reusable builders
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)
Custom config
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Client: &http.Client{
Jar: httpexpect.NewJar(),
Timeout: time.Second * 30,
},
Reporter: httpexpect.NewRequireReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewCurlPrinter(t),
httpexpect.NewDebugPrinter(t, true),
},
})
Session support
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: httpexpect.NewJar(),
},
})
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: nil,
},
})
Use HTTP handler directly
var handler http.Handler = MyHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewBinder(handler),
Jar: httpexpect.NewJar(),
},
})
var handler fasthttp.RequestHandler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewFastBinder(handler),
Jar: httpexpect.NewJar(),
},
})
Per-request client or handler
e := httpexpect.New(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)
Similar packages
Contributing
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
License
MIT