Conex integrates Go testing with Docker and Tart so you can easily run your integration tests and benchmarks.
Yes, we did hear you like integrations.
Integration tests are very good value, they're easy to write and help you catch bugs in a more realistic environment and with most every service and database avaliable as a Docker Container, docker is a great option to run your service dependencies in a clear state. Conex is here to make it simpler by taking care of the following tasks:
- starting containers
- automatically creating uniqe names to avoid conflicts
- deleting containers
- pull or check images before running tests
- Wait for a service (tcp, udp) port to accept connections
- Expose ports
On top of that, Conex providers a driver convention to simplify code reuse across projects.
To use conex, we will leverage TestMain, this will allow us a starting point to connect to docker, pull all the dependent images and only then run the tests.
Simpley call conex.Run(m) where you would run m.Run().
func TestMain(m *testing.M) {
// If you're planing to use conex.Box directly without
// using a driver, you can pass your required images
// after m to conex.Run.
os.Exit(conex.Run(m))
}In our tests, we will use driver packages, these packages register their required image with conex and provide you with a native client and take cares of requesting a container from conex.
Here is an example using redis:
func testPing(t *testing.T) {
redisDb: = 0
client, container := redis.Box(t, redisDb)
defer container.Drop() // Return the container.
// here we can simply use client which is a go-redis
// client.
}You can find find drivers/box packages for redis, mysql, postgresql, rethinkdb, and many more on github.com/conex.
Here is a complete example using a simple Echo service.
You can create many containers and different services as you want, you can also run multiple tests in parallel without conflict, conex creates the containers with uniqe names that consist of the test id, package path, test name, container, and an ordinal index starting from 0. This avoids container name conflicts across the board.
package example_test
import (
"os"
"testing"
"github.com/omeid/conex"
"github.com/omeid/conex/echo"
echolib "github.com/omeid/echo"
)
func TestMain(m *testing.M) {
os.Exit(conex.Run(m))
}
func TestEcho(t *testing.T) {
reverse := true
e, container := echo.Box(t, reverse)
defer container.Drop() // Return the container.
say := "hello"
expect := say
if reverse {
expect = echolib.Reverse(say)
}
reply, err := e.Say(say)
if err != nil {
t.Fatal(err)
}
if reply != expect {
t.Fatalf("\nSaid: %s\nExpected: %s\nGot: %s\n", say, expect, reply)
}
}
// You can also use containers in benchmarks!
func BenchmarkEcho(b *testing.B) {
reverse := false
say := "hello"
expect := say
e, c := echo.Box(b, reverse)
defer c.Drop()
for n := 0; n < b.N; n++ {
reply, err := e.Say(say)
if err != nil {
b.Fatal(err)
}
if reply != expect {
b.Fatalf("\nSaid: %s\nExpected: %s\nGot: %s\n", say, expect, reply)
}
}
}And running tests will yield:
$ go test -v
2017/04/17 22:13:05
=== conex: Pulling Images
--- Pulling omeid/echo:http (1 of 1)
http: Pulling from omeid/echo
627beaf3eaaf: Already exists
8800e3417eb1: Already exists
b6acb96fee14: Already exists
66be5afddf19: Already exists
8ca17cdcfc93: Already exists
792cf0844f5e: Already exists
26601152322c: Pull complete
2cb3c6a6d3ee: Pull complete
Digest: sha256:f6968275ab031d91a3c37e8a9f65b961b5a3df850a90fe4551ecb4724ab3b0a7
Status: Downloaded newer image for omeid/echo:http
=== conex: Pulling Done
2017/04/17 22:13:38
2017/04/17 22:13:38
=== conex: Starting your tests.
=== RUN TestEcho
--- PASS: TestEcho (0.55s)
conex.go:11: creating (omeid/echo:http: -reverse) as conex_508151185_test-TestEcho-omeid_echo.http_0
conex.go:11: started (omeid/echo:http: -reverse) as conex_508151185_test-TestEcho-omeid_echo.http_0
PASS
ok test 33.753sThe Config struct supports Docker-specific options for containers that need elevated access:
c := conex.Box(t, &conex.Config{
Image: "docker:dind",
Privileged: true, // run in privileged mode (e.g. for Docker-in-Docker)
Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, // volume mounts
Env: []string{"DOCKER_TLS_CERTDIR="},
})Privileged and Binds are Docker runner options. The Tart runner ignores them -- VMs are full OS instances that don't need these concepts.
Conex drivers are simple packages that follow a convention to provide a simple interface to the underlying service run on the container. So the user doesn't have to think about containers but the service in their tests.
First, define an image attribute for your package that users can change and register it with conex.
// Image to use for the box.
var Image = "redis:alpine"
func init() {
conex.Require(func() string { return Image })
}Instead of pulling a pre-built image, you can build one from a Dockerfile. Use a path that starts with Dockerfile as the image name:
var Image = "Dockerfile.myservice"
func init() {
conex.Require(func() string { return Image })
}Conex detects Dockerfile paths automatically. The Dockerfile is built before tests run, and the resulting image is tagged conex-build:<name>. Suffixes are supported: Dockerfile.ssh, Dockerfile.testing, etc. The Dockerfile path is relative to the test's working directory.
This is useful when you need a custom test image that isn't available on a registry, or when the image setup requires steps that are too slow to run at container startup (like installing packages).
Then request a container with the required image from conex and setup a client that is connected to the container you created. Return the client and the container.
// Box returns an connect to an echo container based on
// your provided tags.
func Box(t testing.TB, optionally SomeOptions) (your.Client, conex.Container)) {
conf := &conex.Config{
Image: Image,
// Here you may set other options based
// on the options passed to Box.
}
c, con := conex.Box(t, conf)
opt := &your.Options{
Addr: c.Address(),
magic: optionally.SomeMagic,
}
client, err := redis.NewClient(opt)
if err != nil {
t.Fatal(err)
}
return client, con
}Conex automatically detects the appropriate runner based on your environment:
- Linux with local Docker socket: Uses the native runner (direct container IP access)
- macOS, Windows, or remote Docker: Uses the docker runner (runs tests inside a container)
The native runner connects to containers using their direct IP addresses. This is automatically selected on Linux with a local Docker socket.
The docker runner automatically runs your tests inside a Docker container on a shared conex network. This is automatically selected on macOS, Windows, or when using a remote Docker host, since container IPs are not directly accessible in these environments.
When using the docker runner, conex will:
- Create a
conexDocker network - Run your test binary inside a Go container on that network
- All service containers are also created on the same network
- Containers can communicate using their names as hostnames
You can customize the Go image used for running tests:
func TestMain(m *testing.M) {
// Use a specific Go version
conex.GoImage = "golang:1.21-alpine"
os.Exit(conex.Run(m))
}The tart runner creates macOS and Linux VMs using Tart on Apple Silicon Macs. VMs are cloned from base images, started, and deleted automatically just like Docker containers.
CONEX_RUNNER=tart go test ./...Both macOS and Linux images are supported:
Support for running Tart VMs on remote machines via SSH is coming soon.
var image = "ghcr.io/cirruslabs/macos-sequoia-base:latest"
func init() {
conex.Require(func() string { return image })
}
func TestInVM(t *testing.T) {
c := conex.Box(t, &conex.Config{
Image: image,
})
defer c.Drop()
t.Logf("VM running at %s", c.Address())
}You can override the auto-detected runner using the CONEX_RUNNER environment variable:
# Force native runner
CONEX_RUNNER=native go test ./...
# Force docker runner
CONEX_RUNNER=docker go test ./...Yes.
MIT.