conex

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2026 License: MIT Imports: 18 Imported by: 7

README

Conex GoDoc Build Status Go Report Card

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.

Why?

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.

How?

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.
}

Boxes

You can find find drivers/box packages for redis, mysql, postgresql, rethinkdb, and many more on github.com/conex.

Example

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.753s
Advanced Container Options

The 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.

Drivers Packages

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.

Using Registry Images

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 })
}
Using Dockerfiles

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
}

Runners

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)
Native Runner

The native runner connects to containers using their direct IP addresses. This is automatically selected on Linux with a local Docker socket.

Docker Runner

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:

  1. Create a conex Docker network
  2. Run your test binary inside a Go container on that network
  3. All service containers are also created on the same network
  4. 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))
}
Tart Runner (Experimental)

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())
}
Overriding Auto-Detection

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 ./...
Is it good?

Yes.

LICENSE

MIT.

Documentation

Overview

Package conex provides easy to use Docker Integration with Testing.

Index

Constants

View Source
const (
	// ConexNetworkName is the name of the Docker network used for conex containers.
	ConexNetworkName = "conex"
	// ConexRunnerEnv is the environment variable that indicates we're running inside a conex container.
	ConexRunnerEnv = "CONEX_INSIDE_DOCKER"
)

Variables

View Source
var (
	// FailReturnCode is used as status code when conex fails to setup during Run.
	// This does not override the return value of testing.M.Run, only when conex
	// fails to even testing.M.Run.
	FailReturnCode = 255
	// PullImages dictates whatever the Manager should attempt to pull the images
	// on run or simply ensure they exist.
	// Note: Pulling images may result into updates.
	PullImages = true

	// GoImage is the Docker image used to run tests inside a container when
	// using the Docker runner. This should be a Go image that matches your
	// Go version. Set this before calling Run() if you need a specific version.
	// Example: "golang:1.21-alpine"
	GoImage = "golang:1.22"
)
View Source
var ErrPortWaitTimedOut = errors.New("wait timeout")

ErrPortWaitTimedOut is returned when Container.Wait reaches maxWait before the port accepts connections.

Functions

func Require

func Require(images ...func() string)

Require adds the image name returned by the provided functions to the list of images pull by the default Manager when Run is called. Used by driver packages, see conex/redis, conex/rethink.

func Run

func Run(m *testing.M, images ...string) int

Run prepares a docker client, pulls the provided list of images and then runs your tests.

Types

type Config

type Config struct {
	Image      string   // Name of the image as it was passed by the operator (e.g. could be symbolic)
	Env        []string // List of environment variable to set in the container
	Cmd        []string // Command to run when starting the container
	Hostname   string   // Hostname
	Domainname string   // Domainname
	User       string   // User that will run the command(s) inside the container, also support user:group
	Expose     []string // Ports to expose, supports the docker command line style syntax proto/port or just port which defaults to tcp
	Privileged bool     // Run the container in privileged mode
	Binds      []string // Volume binds (e.g. "/host/path:/container/path")
}

Config contains the configuration data about a container.

type Container

type Container interface {
	ID() string
	Name() string
	Image() string
	Address() string

	Drop()

	Wait(port string, timeout time.Duration) error // Wait for the port to respond to tcp/udp.

}

Container is a simple interface to a docker container.

func Box

func Box(t testing.TB, conf *Config) Container

Box creates a new container using the provided image and passes your parameters.

type DockerRunner added in v0.0.2

type DockerRunner struct {
	// contains filtered or unexported fields
}

DockerRunner runs tests inside a Docker container on the same network as other conex containers. This allows conex to work on systems where container IPs are not directly accessible (e.g., Docker for Mac).

func NewDockerRunner added in v0.0.2

func NewDockerRunner(config *RunnerConfig) *DockerRunner

NewDockerRunner creates a new Docker runner.

func (*DockerRunner) Box added in v0.0.2

func (r *DockerRunner) Box(t testing.TB, conf *Config, name string) Container

Box creates a container on the conex network and returns a Container that uses the container name for connections.

func (*DockerRunner) Run added in v0.0.2

func (r *DockerRunner) Run(m *testing.M) int

Run executes the tests. If we're already inside a Docker container (detected by environment variable), it just runs the tests. Otherwise, it creates a container, mounts the current directory, and runs the tests inside it.

type Manager

type Manager interface {
	Run(m *testing.M, images ...string) int
	Box(t testing.TB, config *Config) Container
}

Manager is the conex container manager.

func New

func New(retcode int, pullImages bool, images ...string) Manager

New returns a new conex manager.

type NativeRunner added in v0.0.2

type NativeRunner struct {
	// contains filtered or unexported fields
}

NativeRunner runs tests on the host machine and connects to containers via their IP addresses. This requires native Docker (not Docker for Mac).

func NewNativeRunner added in v0.0.2

func NewNativeRunner(config *RunnerConfig) *NativeRunner

NewNativeRunner creates a new native runner.

func (*NativeRunner) Box added in v0.0.2

func (r *NativeRunner) Box(t testing.TB, conf *Config, name string) Container

Box creates a container and returns a Container that uses the container's direct IP address for connections.

func (*NativeRunner) Run added in v0.0.2

func (r *NativeRunner) Run(m *testing.M) int

Run executes the tests directly on the host.

type Runner added in v0.0.2

type Runner interface {
	// Run executes the test suite. The runner is responsible for setting up
	// any necessary environment and executing m.Run().
	Run(m *testing.M) int

	// Box creates a container and returns a Container interface.
	// The implementation determines how the container is accessed (direct IP vs network alias).
	Box(t testing.TB, conf *Config, name string) Container
}

Runner is an abstraction that allows running tests either natively on the host or inside a Docker container. This enables conex to work on systems where container IPs are not directly accessible from the host (e.g., Docker for Mac).

type RunnerConfig added in v0.0.2

type RunnerConfig struct {
	Client     *docker.Client
	Name       string // prefix for container names
	PullImages bool
	Images     []string
	RetCode    int
	Counter    *counter
	GoImage    string // Go image for running tests in Docker runner
}

RunnerConfig holds configuration for creating a runner.

type RunnerType added in v0.0.2

type RunnerType string

RunnerType specifies which runner implementation to use.

const (
	// RunnerNative runs tests on the host with direct container IP access.
	// This is the default and requires native Docker.
	RunnerNative RunnerType = "native"

	// RunnerDocker runs containers on a shared network, allowing tests to
	// work on systems where container IPs are not accessible from the host
	// (e.g., Docker for Mac, Docker Machine).
	RunnerDocker RunnerType = "docker"
)
const (
	// RunnerTart runs VMs using Tart virtualization.
	// Container IPs are directly accessible from the host.
	RunnerTart RunnerType = "tart"
)

type TartRunner added in v0.0.3

type TartRunner struct {
	// contains filtered or unexported fields
}

TartRunner runs tests on the host machine and manages Tart VMs as containers. VMs are cloned from base images and accessed via their direct IP addresses.

func NewTartRunner added in v0.0.3

func NewTartRunner(config *RunnerConfig) *TartRunner

NewTartRunner creates a new tart runner.

func (*TartRunner) Box added in v0.0.3

func (r *TartRunner) Box(t testing.TB, conf *Config, name string) Container

Box clones a Tart VM from the given image and starts it. The Config.Image field specifies the Tart VM image to clone from. Cmd, Env, and Expose are supported through tart exec after boot.

func (*TartRunner) Run added in v0.0.3

func (r *TartRunner) Run(m *testing.M) int

Run executes the tests directly on the host.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL