apitest is a free, open source api development & testing project written in Go and released under MIT. It has 846 GitHub stars, 58 forks and 0 open issues, and was last pushed 14 days ago. On this registry it ranks #92 of 103 tracked projects in API Development & Testing, with 5 head-to-head comparisons available.

What is apitest?

What it is

apitest is a behavioural testing library for Go HTTP services, published under the MIT licence and maintained at github.com/steinfletcher/apitest. It lives in the Go ecosystem and has been in development for eight years, accumulating 846 stars and 58 forks, with an API the README describes as stable. Tests are written the way a client uses an API: a request is built, sent to a handler, and the response is asserted against expectations. The README states that in behavioural tests the internal structure of the application is not known by the tests; data is input to the system and the outputs must meet certain conditions.

The concrete problem it solves is the boilerplate and repetition involved in testing REST APIs, HTTP handlers and end-to-end flows in Go. Instead of wiring up servers, constructing raw requests and comparing response payloads by hand, apitest offers fluent builders for requests and expectations, and declarative mocks for outbound HTTP calls with matchers for every part of a request. It also renders a sequence diagram of each test, including mocked calls and database queries, so the flow a test exercises is visible rather than inferred from assertions.

Key capabilities

  • Fluent builders that split a test into request description before Expect(t) and expected response after it, ending with End().
  • Works with anything implementing http.Handler — a http.ServeMux, a gin engine or an echo instance — or against a running server.
  • Declarative mocks for outbound HTTP calls, with matchers covering every part of the mocked request.
  • Sequence diagram rendering per test, capturing mocked calls and database queries.
  • Structural JSON comparison, so key order and whitespace do not affect body assertions.
  • No third-party dependencies; integrates with the standard testing package, Ginkgo and custom assertion libraries.
  • Request builders covering methods, URLs, headers, query parameters, cookies, bodies and GraphQL queries.

Who uses it and how

  • Teams writing blackbox tests against REST APIs and HTTP handlers, where the test acts as an API client and does not reach into application internals.
  • Developers running end-to-end tests against a live server through the same request and expectation builders.
  • Engineers testing handlers in process by passing the handler directly, which avoids starting a real listener.
  • Projects that need to isolate outbound HTTP dependencies, using declarative mocks to stub third-party calls and keep tests deterministic.
  • Maintainers who want each test to produce a sequence diagram documenting the calls and database queries it performed.

Getting started

Install the library with go get github.com/steinfletcher/apitest, then build a test that calls apitest.New(), attaches a handler, describes a request and asserts

project readme (upstream, from github) — read inline

Go Reference Build Status Coverage Status Mentioned in Awesome Go

apitest

A simple and extensible behavioural testing library for Go HTTP services. Tests are written the way a client uses the API: build a request, send it to the handler, and assert on the response. External HTTP calls can be mocked, and every test can render a sequence diagram of what happened.

In behavioural tests the internal structure of the app is not known by the tests. Data is input to the system and the outputs are expected to meet certain conditions.

  • Fluent builders for requests and expectations
  • Works with anything that implements http.Handler, or against a running server
  • Declarative mocks for outbound HTTP calls, with matchers for every part of the request
  • Sequence diagrams of each test, including mocked calls and database queries
  • No third party dependencies; integrates with testing, Ginkgo and custom assertion libraries

The API is stable. The library is maintained and issues are addressed; feature requests are considered.

Join the conversation at #apitest on https://gophers.slack.com.

Logo by @egonelbre

Documentation

This README covers the whole library. The same material with more narrative is at https://apitest.dev, and the API reference is on pkg.go.dev.

Installation

go get github.com/steinfletcher/apitest

Quick start

func TestGetUser(t *testing.T) {
	apitest.New().
		Handler(handler).
		Get("/user/1234").
		Expect(t).
		Status(http.StatusOK).
		Body(`{"id": "1234", "name": "Tate"}`).
		End()
}

Handler takes any http.Handler, so a http.ServeMux, a gin engine, an echo instance and so on all work. Everything before Expect(t) describes the request; everything after it describes the expected response. End() runs the test. When the expected body is valid JSON it is compared structurally, so key order and whitespace do not matter.

Demo

animated gif

Building the request

Method and URL

Get, Post, Put, Patch and Delete set the method and URL. Each has an f variant that formats the URL, and Method covers anything else.

apitest.Handler(handler).Getf("/user/%s", id)

apitest.Handler(handler).Method(http.MethodOptions).URL("/user")

A ready-made *http.Request can be used instead of the builder.

req := httptest.NewRequest(http.MethodGet, "/user/1234", nil)

apitest.Handler(handler).
	HttpRequest(req).
	Expect(t).
	Status(http.StatusOK).
	End()

Headers

apitest.Handler(handler).
	Get("/hello").
	Header("Authorization", "Bearer token").
	Headers(map[string]string{"X-Request-Id": "12345"}).
	ContentType("application/json").
	Expect(t).
	Status(http.StatusOK).
	End()

Query parameters

Query, QueryParams and QueryCollection can be combined. QueryCollection sets repeated parameters, so map[string][]string{"a": {"b", "c", "d"}} is encoded as a=b&a=c&a=d.

apitest.Handler(handler).
	Get("/hello").
	QueryParams(map[string]string{"a": "1", "b": "2"}).
	Query("c", "d").
	QueryCollection(map[string][]string{"e": {"f", "g"}}).
	Expect(t).
	Status(http.StatusOK).
	End()

Cookies

apitest.Handler(handler).
	Get("/hello").
	Cookie("session", "12345").
	Cookies(apitest.NewCookie("theme").Value("dark").Path("/")).
	Expect(t).
	Status(http.StatusOK).
	End()

Body

Body sets a raw body. JSON sets the body and the Content-Type header; it accepts a string, a []byte, or any value that can be marshalled. BodyFromFile and JSONFromFile read the body from disk.

apitest.Handler(handler).
	Post("/user").
	JSON(map[string]any{"name": "jan", "age": 32}).
	Expect(t).
	Status(http.StatusCreated).
	End()

GraphQL requests are built with GraphQLQuery, or GraphQLRequest when an operation name is needed.

apitest.Handler(handler).
	Post("/graphql").
	GraphQLQuery(`query User($id: ID!) { user(id: $id) { name } }`, map[string]any{"id": "1234"}).
	Expect(t).
	Status(http.StatusOK).
	End()

Form data

FormData sends an application/x-www-form-urlencoded body.

apitest.Handler(handler).
	Post("/hello").
	FormData("a", "1").
	FormData("b", "2", "3").
	Expect(t).
	Status(http.StatusOK).
	End()

MultipartFormData and MultipartFile send multipart/form-data. The two form styles cannot be combined in one request.

apitest.Handler(handler).
	Post("/upload").
	MultipartFormData("description", "holiday photos").
	MultipartFile("file", "testdata/beach.jpg", "testdata/sunset.jpg").
	Expect(t).
	Status(http.StatusOK).
	End()

Files are read from the OS by default. UseFS swaps in any fs.FS, such as an in-memory fstest.MapFS.

inMemFS := fstest.MapFS{
	"audio.wav": &fstest.MapFile{Data: []byte{19, 2, 123, 12, 35, 1}},
}

apitest.Handler(handler).
	UseFS(inMemFS).
	Post("/upload").
	MultipartFile("file", "audio.wav").
	Expect(t).
	Status(http.StatusOK).
	End()

Basic auth

apitest.Handler(handler).
	Get("/hello").
	BasicAuth("username", "password").
	Expect(t).
	Status(http.StatusOK).
	End()

Context

WithContext sets the request context, which is how deadlines, cancellation and context values reach the handler.

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

apitest.Handler(handler).
	Get("/hello").
	WithContext(ctx).
	Expect(t).
	Status(http.StatusOK).
	End()

Intercept

Intercept receives the built *http.Request just before it is sent, for changes the builder does not cover.

apitest.Handler(handler).
	Intercept(func(req *http.Request) {
		req.URL.RawQuery = "a[]=xxx&a[]=yyy"
	}).
	Get("/hello").
	Expect(t).
	Status(http.StatusOK).
	End()

Asserting on the response

Status and body

apitest.Handler(handler).
	Get("/user/1234").
	Expect(t).
	Status(http.StatusOK).
	Body(`{"id": "1234", "name": "Tate"}`).
	End()

A JSON body is compared structurally. Any other body is compared as an exact string. Bodyf formats the expected body and BodyFromFile reads it from disk.

Headers

apitest.Handler(handler).
	Get("/hello").
	Expect(t).
	Status(http.StatusOK).
	Header("Content-Type", "application/json").
	Headers(map[string]string{"X-Request-Id": "12345"}).
	HeaderPresent("Etag").
	HeaderNotPresent("X-Powered-By").
	End()

Cookies

Only the fields set on the expected cookie are compared, so NewCookie("session").Value("12345") ignores the path, expiry and other attributes of the actual cookie.

apitest.Handler(handler).
	Patch("/hello").
	Expect(t).
	Status(http.StatusOK).
	Cookie("session", "12345").
	Cookies(apitest.NewCookie("theme").Value("dark").HttpOnly(true)).
	CookiePresent("csrf").
	CookieNotPresent("legacy").
	End()

Custom assertions

Assert takes a function that receives copies of the response and request and returns an error on failure. It can be called several times.

apitest.Handler(handler).
	Get("/hello").
	Expect(t).
	Assert(func(res *http.Response, req *http.Request) error {
		if res.Header.Get("X-Rate-Limit") == "" {
			return errors.New("expected a rate limit header")
		}
		return nil
	}).
	End()

apitest.IsSuccess, apitest.IsClientError and apitest.IsServerError are ready-made assertions on the status code range.

JSONPath

For asserting on parts of the response body, the separate apitest-jsonpath module provides JSONPath assertions. It is packaged separately to keep this library dependency free.

Given the response {"a": 12345, "b": [{"key": "c", "value": "result"}]}:

apitest.Handler(handler).
	Get("/hello").
	Expect(t).
	Assert(jsonpath.Contains(`$.b[? @.key=="c"].value`, "result")).
	Assert(jsonpath.Equal(`$.a`, float64(12345))).
	End()

The result

End() returns a Result holding the response, so further checks can be made after the test has run.

result := apitest.Handler(handler).
	Get("/user/1234").
	Expect(t).
	Status(http.StatusOK).
	End()

var user struct {
	Name string `j

readme truncated — read the full docs on github

Frequently asked questions

Is apitest free to use?

apitest is open source under the MIT licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does apitest do?

A simple and extensible behavioural testing library for Go. You can use api test to simplify REST API, HTTP handler and e2e tests.

What is apitest written in?

apitest is primarily written in Go. Its source is publicly available at https://github.com/steinfletcher/apitest, and it has 846 GitHub stars.