
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:
go-interpol package.go-querystring package).form package), plain text.jsonpath package.gojsonschema package.gorilla/websocket internally).gojsondiff package.testify (assert or require package) or standard testing package.encoding/json, Go values are pretty-printed using litter.httputil, http2curl, or simple compact logger.fatih/color.net/http or fasthttp handler directly.text/template engine.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 iris framework. Example includes JSON queries and validation, URL and form parameters, basic auth, sessions, and streaming. Tests invoke the http.Handler directly.
Testing a server with JWT authentication made with echo framework. Tests use either HTTP client or invoke the http.Handler directly.
Testing a server utilizing the gin web framework. Tests invoke the http.Handler directly.
Testing a server made with fasthttp package. Tests invoke the fasthttp.RequestHandler directly.
Testing a WebSocket server based on gorilla/websocket. Tests invoke the http.Handler or fasthttp.RequestHandler directly.
Testing a TLS server made with net/http and crypto/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)
```go 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
$ claude mcp add httpexpect \
-- python -m otcore.mcp_server <graph>