A Go library for orchestrating multi-service test environments. Built on
testcontainers-go, testrig adds
parallel lifecycle management and property aggregation across
services so an integration test can declare what it needs and let the
framework handle bringing it up, wiring it into the application's config,
and tearing it down.
Status: pre-1.0, API not yet stable. Module path:
github.com/sha1n/testrig.
The engine and each pre-built service are independently versioned Go modules. Add only what you need:
# Engine — required. Stdlib + golang.org/x/sync only.
go get github.com/sha1n/testrig@v0.0.0-prototype.1
# Pre-built services (each pulls its own deps; pick what you use):
go get github.com/sha1n/testrig/services/postgres@v0.0.0-prototype.1
go get github.com/sha1n/testrig/services/wiremock@v0.0.0-prototype.1
go get github.com/sha1n/testrig/services/oidc@v0.0.0-prototype.1While the API is still iterating, the only published tags are
v0.0.0-prototype.* prereleases. Pin explicitly rather than using
@latest: today @latest resolves to the highest prerelease, but the
moment a non-prerelease tag (e.g. v0.1.0) is published, @latest
will silently switch to it and skip all prereleases — possibly across
breaking changes while pre-1.0.
The examples/ and tools/ modules are repository-internal (demo apps and
the pinned golangci-lint for make lint respectively); they are not
intended to be imported by external projects and have no published tags.
Tests that need a real Postgres, a stubbed HTTP backend, or a handful of
collaborating services typically end up with hand-rolled TestMain scaffolding:
spin up containers, aggregate host/port/credentials into the app's config,
tear everything down on exit. testrig factors that scaffolding into an
Env orchestrator that brings services up in parallel, gathers their published
properties, and tears them down on rollback or test exit.
import (
"context"
"testing"
"github.com/sha1n/testrig"
"github.com/sha1n/testrig/services/postgres"
"github.com/stretchr/testify/require"
)
func TestSomething(t *testing.T) {
// Publish the DSN directly under the application's expected config key.
pg := postgres.New("pg").
WithDatabase("appdb").
WithDSNPropertyName("DATABASE_URL")
env := testrig.New("test").With(pg)
props, err := env.Start(context.Background())
require.NoError(t, err)
t.Cleanup(func() { _ = env.Stop(context.Background()) })
// Properties are ready to merge into your app's config loader.
dsn := props["DATABASE_URL"]
_ = dsn
// Or use the typed accessor on the service.
db, err := pg.DB(context.Background())
require.NoError(t, err)
defer db.Close()
// ... run your test against db / dsn
}- Parallel start, parallel stop. All services start concurrently; rollback on failure stops only those whose Start succeeded.
- Property aggregation. Services publish a
Propertiesmap (host, port, credentials, DSNs);env.Properties()returns a stable snapshot for the test to read. - App-aligned property keys. Each pre-built service supports
WithXxxPropertyName(...)so its outputs land directly under the application's expected config keys — no bridging step in the test. - Pluggable injection. Pass
env.Properties()to viper, koanf, or anymap[string]string-shaped config; or useSetEnvVars(t, props)for libraries that read only fromos.Getenv. - Opt-in startup ordering. Use
testrig.NewStages(a).Then(b, c)andenv.WithStages(...)when you need explicit ordering between groups of services. - Pre-built services.
services/postgres,services/wiremock, andservices/oidcship as ready-to-use implementations (the first two testcontainers-backed; OIDC is a non-dockerized in-process issuer). New services are a singleServiceinterface implementation away (3 methods:Name,Start,Stop).
Each pre-built service is a separate Go module. go get github.com/sha1n/testrig
pulls only the engine; testcontainers, pgx, JWT, and other heavy deps come in
only when you import a service.
Each service has its own README with a quickstart, full configuration reference, and a "Gaps and workarounds" section.
| Service | Import | Notes |
|---|---|---|
| PostgreSQL | github.com/sha1n/testrig/services/postgres |
testcontainers-backed; exposes DSN() and DB(ctx) once started; all property keys customizable. |
| WireMock | github.com/sha1n/testrig/services/wiremock |
testcontainers-backed; exposes URL() and Client(); URL property key customizable. |
| OIDC | github.com/sha1n/testrig/services/oidc |
non-dockerized, Auth0-style OIDC issuer; supports authorization_code (with PKCE S256), client_credentials, and refresh_token grants; serves discovery, JWKS, /authorize, /token, /userinfo. |
Two parallel example apps demonstrating testrig with different config libraries. Each is a small, well-structured Go server with:
main.go— thin entry pointconfig/— typed config loader (Viper or koanf — the only divergent piece)testenv/— testrig wiring used by bothmainand the testsserver/server_test.go— integration tests sharing one env viaTestMain
The HTTP server itself and the custom schema-seed testrig.Service are
shared between the two examples in examples/internal/sampleapp and
examples/internal/seed — they have no config-library opinion. The seed
package is the canonical demo of how to write your own
non-dockerized testrig.Service and order it after a dependency via
WithStages.
| Example | Config library |
|---|---|
examples/viper-app |
Viper |
examples/koanf-app |
koanf |
The full public-API and runtime-semantics specification lives at
docs/SPEC.md.
This repo is a Go multi-module workspace. Each module is independently versioned and published, so consumers download only the dependencies they actually use.
. github.com/sha1n/testrig (engine; stdlib + golang.org/x/sync)
services/oidc/ github.com/sha1n/testrig/services/oidc
services/postgres/ github.com/sha1n/testrig/services/postgres
services/wiremock/ github.com/sha1n/testrig/services/wiremock
examples/ github.com/sha1n/testrig/examples (not published)
tools/ github.com/sha1n/testrig/tools (not published; pins golangci-lint)
go.work ties all of the above together for local development
Each sub-module imports the engine via github.com/sha1n/testrig, and
each sub-module go.mod carries a require github.com/sha1n/testrig <engine version> plus a replace github.com/sha1n/testrig => ../...
The replace lets local builds work before any engine version is
published; it has no effect on external consumers (Go ignores replace
directives in dependency modules). go.work ties all modules together
for editor and tool support.
Each module is versioned independently using the standard Go sub-module tag scheme:
| Module | Tag prefix | Example |
|---|---|---|
| root | (none) | v0.1.0 |
services/oidc |
services/oidc/ |
services/oidc/v0.1.0 |
services/postgres |
services/postgres/ |
services/postgres/v0.1.0 |
services/wiremock |
services/wiremock/ |
services/wiremock/v0.1.0 |
Release order: tag the engine first, then bump each sub-module's
require github.com/sha1n/testrig to the engine's new version, commit,
then tag the sub-modules. The replace line stays in place — it's
inert for consumers and keeps local development working.
make check # format + lint + test across every module (default)
make test # tests only (every module)
make lint # go vet + golangci-lint across every module; fails on issues
make build-examples # build example binaries into bin/
make go-get # workspace sync across every module
make lint invokes golangci-lint via Go's tool directive — the version is
pinned in tools/go.mod, so contributors don't need to install it separately
and CI runs the same version.
Requires Go 1.25 or later. Tests require Docker (testcontainers).