Skip to content

fabiotavarespr/httpexpect

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

httpexpect GoDev Coveralls GitHub release

Basically, hhis project is a gavv/httpexpect clone with some package updates

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
Response assertions
  • Response status, predefined status ranges.
  • Headers, cookies, payload: JSON, JSONP, forms, text.
  • Round-trip time.
  • Custom reusable response matchers.
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.
WebSocket support (thanks to @tyranron)
  • Upgrade an HTTP connection to a WebSocket connection (we use gorilla/websocket internally).
  • Interact with the WebSocket server.
  • Inspect WebSocket connection parameters and WebSocket messages.
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.

If you're using go.mod, use a versioned import path:

import "github.com/fabiotavarespr/httpexpect"

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.

  • gin_test.go

    Testing a server utilizing the gin web framework. Tests invoke the http.Handler directly.

  • fasthttp_test.go

    Testing a server made with fasthttp package. Tests invoke the fasthttp.RequestHandler directly.

  • websocket_test.go

    Testing a WebSocket server based on gorilla/websocket. Tests invoke the http.Handler or 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/fabiotavarespr/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()
}
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()

// validate JSON schema
repos.Schema(schema)

// run JSONPath query and iterate results
for _, private := range repos.Path("$..private").Array().Iter() {
	private.Boolean().False()
}
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").DateTime().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().Equal(sessionID)
c.Domain().Equal("example.com")
c.Path().Equal("/")
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.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")
Redirection support
e.POST("/path").
	WithRedirectPolicy(httpexpect.FollowAllRedirects).
	WithMaxRedirects(5).
	Expect().
	Status(http.StatusOK)

e.POST("/path").
	WithRedirectPolicy(httpexpect.DontFollowRedirects).
	Expect().
	Status(http.StatusPermanentRedirect)
Retry support
// default retry policy
e.POST("/path").
	WithMaxRetries(5).
	Expect().
	Status(http.StatusOK)

// custom 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)
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)
WebSocket support
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()
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)
Reusable matchers
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)
Request transformers
e := httpexpect.New(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)
Custom config
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),
	},
})
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.NewJar(),
	},
})

// 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.NewJar(),
	},
})
Per-request client or handler
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)
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.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,
	},
})
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{},
		},
	},
})
Proxy support
e := httpexpect.WithConfig(httpexpect.Config{
	Reporter: httpexpect.NewAssertReporter(t),
	Client: &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyURL("http://proxy.example.com"),
		},
	},
})
Global time-out/cancellation
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 time-out/cancellation
// 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)

Similar packages

Contributing

Feel free to report bugs, suggest improvements, and send pull requests! Please add documentation and tests for new features.

Build code, run linters, run tests:

make

Format code:

make fmt

Run go mod tidy:

make tidy

License

MIT