An API integration test framework - for testing APIs written in Go, using a framework written in Go, with tests written in Go.
-
Composable DSL for end-to-end API scenarios
Describe tests as readable, fluent flows – no test YAML, no magic, just Go. -
Black-box testing of any API
Hit a real HTTP endpoint and assert on both the response and what it did to its dependencies. Works regardless of what language/framework the API is written in. -
Real dependency “wraps” (not mocks)
Spin up real services as test dependencies (e.g. MongoDB, LocalStack/AWS services, message brokers) and assert on their state and events. -
Full coverage support
Execute the API tests and optionally report endpoint coverage (even against a provided OAS spec)
Coverage reports can also supply timings (inc. averages, variance, P50, P90, P99) - with built-in support for repeated runs. -
Resolvable values
Uniform mechanism for “values that come from somewhere”: JSON fields, variables, database queries, message payloads, lengths, first/last elements, etc. -
Powerful assertions
Rich set of assertion helpers over response payloads, headers, status codes, dependency state, and captured events. -
Variables
Declare vars once,SetVar()from responses or dependencies, and reuse them across steps. IDE refactoring-friendly because var names are just Go identifiers. -
Conditional flows
If(...)/IfNot(...)blocks let you express conditional logic directly in the DSL without branching mess in test code. -
Capturing and inspecting side effects
Capture SNS/SQS-style publishes, queue messages, Mongo changes, etc., and assert on count, content, and ordering. -
Composable helpers
Small building blocks (values, assertions, conditionals, captures) that can be combined arbitrarily – everything is designed to be reused and extended. -
Extensible dependency model
Built-in examples (Mongo, LocalStack, etc.) double as templates for adding your own dependency “wraps” with minimal boilerplate. -
First-class Go testing integration
Plays nicely withtesting.T, subtests, and your existing tooling; tests are just Go code, no additional runner or framework ceremony. -
Build freshness guard (
makeintegration)
Optionally run amaketarget before tests, so every scenario runs against the latest build artefact instead of whatever binary happened to be lying around. -
IDE-friendly & CI/CD-friendly
Runs identically in GoLand, VS Code, your terminal, GitHub Actions, GitLab CI, Jenkins, or any CI/CD pipeline.
No custom runners, no plugins, no hidden runtime - justgo test.
The intent of the design is to describe API tests in a non-abstract DSL (Domain Specific Language) that can be used by developers and QAs alike.
We've specifically avoided terms like "scenario" - instead using terms like "endpoint" & "method" to describe what's being tested.
Full working demo examples can be found in Petstore and Petstore with db and API image
An example of what the tests definition looks like...
package main
import (
. "github.com/go-andiamo/marrow"
)
var endpointTests = []Endpoint_{
Endpoint("/api", "Root",
Method(GET, "Get root").
AssertOK().
AssertEqual(JsonPath(Body, "hello"), "world"),
Endpoint("/categories", "Categories",
Method(GET, "Get first category id (used for creating pet)").
RequireOK().
RequireGreaterThan(JsonPath(Body, LEN), 0).
SetVar(After, "category-id", JsonPath(JsonPath(Body, "0"), "id")),
),
Endpoint("/pets", "Pets",
Method(GET, "Get pets (empty)").
AssertOK().
AssertLen(Body, 0),
Method(POST, "Create pet").
RequestBody(JSON{
"name": "Felix",
"dob": "2025-11-01",
"category": JSON{
"id": Var("category-id"),
},
}).
AssertCreated().
SetVar(After, "created-pet-id", JsonPath(Body, "id")),
Endpoint("/{petId}", "Pet",
Method(GET, "Get pet (not found)").
PathParam(Var("non-uuid")).
AssertNotFound(),
Method(PUT, "Update pet (not found)").
PathParam(Var("non-uuid")).
AssertNotFound(),
Method(PUT, "Update pet successful").
PathParam(Var("created-pet-id")).
RequestBody(JSON{
"name": "Feline",
"dob": "2025-11-02",
"category": JSON{
"id": Var("category-id"),
},
}).
AssertOK(),
Method(DELETE, "Delete pet (not found)").
PathParam(Var("non-uuid")).
AssertNotFound(),
Method(DELETE, "Delete pet successful").
PathParam(Var("created-pet-id")).
AssertNoContent(),
),
),
Endpoint("/categories", "Categories",
Method(GET, "Get categories").
AssertOK().
AssertGreaterThan(JsonPath(Body, LEN), 0),
Endpoint("/{categoryId}", "Category",
Method(GET, "Get category (not found)").
PathParam(Var("non-uuid")).
AssertNotFound(),
Method(GET, "Get category (found)").
PathParam(Var("category-id")).
AssertOK(),
),
),
),
}And to run the endpoints tests...
package main
import (
. "github.com/go-andiamo/marrow"
)
func main() {
s := Suite(endpoints...)
err := s.Init(/* whatever initializers needed */).Run()
if err != nil {
panic(err)
}
}Or to run as part of Go tests...
package main
import (
. "github.com/go-andiamo/marrow"
"github.com/go-andiamo/marrow/with"
"testing"
)
func TestApi(t *testing.T) {
s := Suite(endpoints...)
err := s.Init(
with.Testing(t),
/* whatever other initializers needed */).Run()
if err != nil {
t.Fatal(err)
}
}go get github.com/go-andiamo/marrow
Marrow comes with several ready-rolled supporting images for common dependencies (more to come)...
- Artemis
go get github.com/go-andiamo/marrow/images/artemis - Dragonfly (drop-in replacement for Redis)
go get github.com/go-andiamo/marrow/images/dragonfly - Kafka
go get github.com/go-andiamo/marrow/images/kafka - AWS localstack
go get github.com/go-andiamo/marrow/images/localstack - MongoDB
go get github.com/go-andiamo/marrow/images/mongo - MySql
go get github.com/go-andiamo/marrow/images/mysql - Nats
go get github.com/go-andiamo/marrow/images/nats - Postgres
go get github.com/go-andiamo/marrow/images/postgres - Redis
go get github.com/go-andiamo/marrow/images/redis7
Support images can also be used independently of Marrow in unit testing - see example