Conex integrates Go testing with Docker (and Tart, experimentally) so integration tests can start real dependencies with less boilerplate.
Integration tests are high-value when they run against real services. Conex handles common setup work so tests stay focused on behavior:
- Start and stop containers
- Create unique names to avoid collisions
- Pull images (or build from Dockerfiles) before tests run
- Wait for TCP/UDP ports to accept connections
- Expose ports
It also supports a driver convention so reusable test helpers can register their required images.
To use conex, simply call conex.Main(m) from TestMain:
package example_test
import (
"testing"
"github.com/omeid/conex"
)
func TestMain(m *testing.M) {
conex.Main(m)
}package example_test
import (
"testing"
"github.com/omeid/conex"
"github.com/conex/postgres"
_ "github.com/lib/pq" // Bring your own driver!
)
func TestMain(m *testing.M) {
conex.Main(m)
}
func TestPostgres(t *testing.T) {
db, container := postgres.Box(t, nil)
_ = db
// use db to interact with the postgresql database
// you can also execute commands directly inside the container
// using an API that closely matches os/exec:
// cmd := container.Exec("psql", "-U", "postgres", "-c", "CREATE DATABASE testdb;")
// out, err := cmd.CombinedOutput()
}Config supports Docker-specific container options:
c := conex.Box(t, &conex.Config{
Image: "docker:dind",
Privileged: true,
Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"},
})Privileged and Binds are Docker runner options only.
Conex drivers are small packages that wrap a service container with a native client API. This lets tests focus on the service instead of raw container lifecycle details.
A driver usually:
- Defines an
Imagevariable - Registers it with
conex.Require(...) - Exposes a helper that returns both a client and a
conex.Container
See the echo box source for a concrete example.
Available boxes from github.com/conex/*:
An image can be either:
- A registry reference:
name[:tag|@digest] - A Dockerfile path:
DockerfileorDockerfile.suffix
Before tests run, Conex either pulls/builds these images or validates they already exist, based on configuration (conex.OptPullImages and conex.OptBuildImages).
Conex auto-detects the runner unless specified via conex.OptRunnerType:
- Linux + local Docker socket: native runner (direct container IP)
- macOS/Windows/remote Docker: docker runner (tests run in a container)
Runs tests on the host and connects directly to container IPs.
Runs tests inside a container on a shared conex network. This avoids host-network limitations on Docker Desktop and remote Docker hosts.
When using the docker runner, Conex:
- Creates a
conexnetwork - Runs the test binary in a Go container on that network
- Starts service containers on the same network
- Lets containers communicate via container names
Customize the Go image used by the docker runner:
func TestMain(m *testing.M) {
conex.Main(
m,
conex.OptGoImage("golang:1.21-alpine"),
)
}The Tart runner creates macOS/Linux VMs using Tart on Apple Silicon Macs.
CONEX_RUNNER=tart go test ./...Tart image references should be Tart VM images (for example, ghcr.io/cirruslabs/macos-sequoia-base:latest). Dockerfile image refs are not supported with the Tart runner.
While Conex auto-detects the runner by default, you can explicitly override it using an environment variable:
# Force native runner
CONEX_RUNNER=native go test ./...
# Force docker runner
CONEX_RUNNER=docker go test ./...Alternatively, you can specify the runner programmatically in your tests using conex.OptRunnerType:
func TestMain(m *testing.M) {
conex.Main(
m,
conex.OptRunnerType(conex.RunnerDocker), // Explicitly force the Docker runner
)
}You can configure Conex per run:
func TestMain(m *testing.M) {
conex.Main(
m,
conex.OptPullImages(true),
conex.OptBuildImages(true),
conex.OptGoImage("golang:1.22"),
conex.OptReturnCode(255),
)
}