testworld

package module
v0.0.11 Latest Latest
Warning

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

Go to latest
Published: Feb 28, 2026 License: MIT Imports: 25 Imported by: 0

README

testworld-go

testworld-go is a system test framework based on testcontainers-go. Testcontainers is very flexible, but needs a lot of boilerplate for every test. Testworld cuts down on the boilerplate with an opinionated approach.

Features

  • Async: Containers are created asynchronously, leading to faster tests when more than one container is used.
  • Test isolation: Each test creates a separate namespace and docker bridge network.
  • Replicas: Create and control groups of identical containers.
  • Network isolation: Optionally block a container's internet access while keeping intra-world communication intact.
  • Automatic TLS: Every container receives a TLS certificate signed by a per-world CA, so that all containers can communicate over TLS.
  • Low boilerplate: Reduced boilerplate compared to testcontainers-go
  • Log collection: Collect logs from all containers and output to a verbose log file.
  • Event tracking: Outputs a timeline of events during the test.

Installation

go get github.com/AlveElde/testworld-go

Quick Start

package mytest

import (
    "testing"

    "github.com/AlveElde/testworld-go"
    "github.com/testcontainers/testcontainers-go/wait"
)

func TestWebCluster(t *testing.T) {
    w := testworld.New(t, "")
    defer w.Destroy()

    // Spin up 3 web servers and a client — all 4 containers are created in parallel
    servers := w.NewContainer(testworld.ContainerSpec{
        Image:      "caddy:latest",
        Replicas:   3,
        WaitingFor: wait.ForHTTP("/").WithPort("80/tcp"),
    })

    client := w.NewContainer(testworld.ContainerSpec{
        Image:     "alpine:latest",
        KeepAlive: true,
        After:     []testworld.WorldContainer{servers},
    })

    // The group name resolves to all 3 server IPs via DNS round-robin.
    // After ensures servers are ready before Exec runs.
    client.Exec([]string{"wget", "-q", "-O", "/dev/null", "http://" + servers.Name}, 0)
}

API Reference

World
// Create a new World. Pass a directory path to enable logging, or "" to disable.
w := testworld.New(t, "/path/to/logs")
defer w.Destroy()
ContainerSpec
spec := testworld.ContainerSpec{
    // Image to use (e.g., "alpine:latest")
    Image: "alpine:latest",

    // Or build from Dockerfile
    FromDockerfile: testcontainers.FromDockerfile{
        Context: "./docker/myapp",
    },

    // Create multiple identical containers as a group (default: 1)
    Replicas: 3,

    // Keep the container running indefinitely (uses "sleep infinity" when no Cmd is set)
    KeepAlive: true,

    // Override entrypoint
    Entrypoint: []string{"/entrypoint.sh"},

    // Command to run (overrides KeepAlive)
    Cmd: []string{"myapp", "--config", "/etc/myapp.conf"},

    // Environment variables
    Env: map[string]string{"DEBUG": "true"},

    // Exposed ports
    ExposedPorts: []string{"8080/tcp"},

    // Files to copy into the container
    Files: []testcontainers.ContainerFile{...},

    // Tmpfs mounts
    Tmpfs: map[string]string{"/tmp": ""},

    // Wait strategy for readiness
    WaitingFor: wait.ForHTTP("/health"),

    // Extra DNS aliases
    Aliases: []string{"db", "primary"},

    // Extra subdomain aliases (creates "tenant1.db", "tenant2.db", etc.)
    Subdomains: []string{"tenant1", "tenant2"},

    // Block creation until dependencies are ready (see Dependencies below)
    Requires: []testworld.WorldContainer{db},

    // Create in parallel, but block methods until dependencies are ready
    After: []testworld.WorldContainer{db},

    // Block internet access (see Network Isolation below)
    Isolated: true,

    // Advanced: modify container config
    ConfigModifier: func(c *container.Config) { ... },

    // Advanced: modify host config (mounts, privileged, etc.)
    HostConfigModifier: func(hc *container.HostConfig) { ... },

    // Optional: callback when container is destroyed
    OnDestroy: func(c testworld.WorldContainer) {
        // Collect log files from the container
        c.LogFile("/var/log/app.log")
    },
}
WorldContainer

Containers are created asynchronously. All methods on WorldContainer transparently wait for the container to be ready before proceeding:

// These return immediately — both containers are created in parallel
db := w.NewContainer(dbSpec)
app := w.NewContainer(appSpec)

// First method call on each container blocks until it is ready
db.Wait(wait.ForLog("database system is ready to accept connections"))
app.Wait(wait.ForHTTP("/healthz").WithPort("8080/tcp"))

// Execute a command (fails test if exit code doesn't match)
app.Exec([]string{"curl", "-sf", "http://localhost:8080/healthz"}, 0)

// Block until ready without performing any action
app.Await()

// Copy a file from container to the world log
app.LogFile("/var/log/app.log")
Replicas

Set Replicas to create a group of identical containers. All methods on the WorldContainer execute across every replica. The group name resolves to all replica IPs via Docker DNS round-robin:

servers := w.NewContainer(testworld.ContainerSpec{
    Image:      "caddy:latest",
    Replicas:   3,
    WaitingFor: wait.ForHTTP("/").WithPort("80/tcp"),
})

client := w.NewContainer(testworld.ContainerSpec{
    Image:     "alpine:latest",
    KeepAlive: true,
    After:     []testworld.WorldContainer{servers},
})

// The group name resolves to all 3 server IPs.
// After ensures servers are ready before Exec runs.
client.Exec([]string{"wget", "-q", "-O", "/dev/null", "http://" + servers.Name}, 0)

Each replica also gets its own unique name (servers.Name + "-1", -2, etc.) for individual addressing.

Dependencies

Use Requires and After to declare ordering between containers:

Field Creation Methods (Exec, Await, ...)
Requires Waits for deps Waits for deps
After Runs in parallel Waits for deps

After is the common choice — containers are created in parallel for speed, but methods block until dependencies are ready:

db := w.NewContainer(testworld.ContainerSpec{
    Image:      "postgres:latest",
    WaitingFor: wait.ForLog("ready to accept connections"),
})

app := w.NewContainer(testworld.ContainerSpec{
    Image: "myapp:latest",
    After: []testworld.WorldContainer{db},
})

// Both containers are created in parallel.
// Exec blocks until db is ready.
app.Exec([]string{"curl", "-sf", "http://localhost:8080/healthz"}, 0)

Requires delays creation itself, useful when a container can't be built without the dependency (e.g., pulling from a registry that another container provides):

registry := w.NewContainer(registrySpec)

app := w.NewContainer(testworld.ContainerSpec{
    Image:    "myregistry:5000/myapp:latest",
    Requires: []testworld.WorldContainer{registry},
})

If any dependency fails, containers that depend on it also fail.

Network Isolation

Set Isolated: true on a ContainerSpec to block that container's access to the internet while keeping intra-world communication intact.

Internally, testworld maintains two Docker bridge networks:

Network Internet Intra-world
External (regular bridge) yes yes
Internal (--internal bridge) no yes

Every container joins the internal network. Non-isolated containers also join the external network, gaining internet access via its gateway. Isolated containers join only the internal network — Docker omits the default gateway for --internal networks, so any attempt to reach an external address fails immediately with "Network unreachable".

// A mock server that should never call out to the real internet
mock := w.NewContainer(testworld.ContainerSpec{
    Image:    "my-mock-server:latest",
    Isolated: true,
})

// A regular client that can reach both the internet and the mock server
client := w.NewContainer(testworld.ContainerSpec{
    Image:     "alpine:latest",
    KeepAlive: true,
    After:     []testworld.WorldContainer{mock},
})

// The client can reach the mock server by name
client.Exec([]string{"wget", "-q", "-O", "/dev/null", "http://" + mock.Name + ":8080/"}, 0)

// The mock server cannot reach the internet
mock.Exec([]string{"ping", "-c", "1", "-W", "2", "8.8.8.8"}, 1)

TLS

Every world generates an ephemeral certificate authority. Each container receives a leaf certificate signed by that CA, and the CA is installed into the system trust store. This means containers can talk to each other over HTTPS without any extra configuration:

caddyfile := `{
    auto_https off
}
:8443 {
    tls /tls/cert.pem /tls/key.pem
    respond "Hello TLS"
}`

server := w.NewContainer(testworld.ContainerSpec{
    Image: "caddy:latest",
    Files: []testcontainers.ContainerFile{{
        Reader:            strings.NewReader(caddyfile),
        ContainerFilePath: "/etc/caddy/Caddyfile",
    }},
    WaitingFor: wait.ForLog("serving initial configuration"),
})

client := w.NewContainer(testworld.ContainerSpec{
    Image:     "alpine/curl:latest",
    KeepAlive: true,
    Requires:  []testworld.WorldContainer{server},
})

client.Exec([]string{"curl", "-sf", "https://" + server.Name + ":8443/"}, 0)

Certificates are mounted at well-known paths inside every container:

Path Description
/tls/ca.crt World CA certificate
/tls/cert.pem Container's leaf certificate
/tls/key.pem Container's private key

The environment variables TLS_CA_CERT, TLS_CERT, and TLS_KEY point to these paths. Leaf certificates include SANs for all of the container's DNS names (container name, replica names, and any extra aliases) plus localhost and 127.0.0.1.

World Log

When a log path is provided, the World creates:

  • An ASCII Gantt chart showing event timelines
  • A combined log file with all container outputs

Example output:

Event Timeline (Total: 3.284s):
ID  | Process Visualization
----|--------------------------------------------------------------------------------
000 |[################] (0.672s) World: Create
001 |                [#############################] (1.199s) World: add caddy container TestReplicaHTTPClients-caddy-1-1
002 |                [#############################] (1.210s) TestReplicaHTTPClients-caddy-1: await
003 |                [############################] (1.173s) World: add caddy container TestReplicaHTTPClients-caddy-1-2
004 |                [###########################] (1.121s) World: add curl container TestReplicaHTTPClients-curl-1-1
005 |                [#############################] (1.210s) World: add caddy container TestReplicaHTTPClients-caddy-1-3
006 |                [######################] (0.941s) World: add curl container TestReplicaHTTPClients-curl-1-2
007 |                [###################] (0.804s) World: add curl container TestReplicaHTTPClients-curl-1-3
008 |                                             [##] (0.102s) TestReplicaHTTPClients-curl-1-3: exec curl http://TestReplicaHTTPClients-caddy-1:80/
009 |                                             [##] (0.102s) TestReplicaHTTPClients-curl-1-1: exec curl http://TestReplicaHTTPClients-caddy-1:80/
010 |                                             [##] (0.102s) TestReplicaHTTPClients-curl-1-2: exec curl http://TestReplicaHTTPClients-caddy-1:80/
011 |                                                [#] (0.000s) TestReplicaHTTPClients-caddy-1: await
012 |                                                [#] (0.000s) TestReplicaHTTPClients-curl-1: await
013 |                                                [###############################] (1.300s) World: destroy
014 |                                                [#] (0.003s) TestReplicaHTTPClients-curl-1-3: logs
015 |                                                [#] (0.003s) TestReplicaHTTPClients-curl-1-1: logs
016 |                                                [#] (0.005s) TestReplicaHTTPClients-caddy-1-3: logs
017 |                                                [#] (0.005s) TestReplicaHTTPClients-curl-1-2: logs
018 |                                                [#] (0.006s) TestReplicaHTTPClients-caddy-1-1: logs
019 |                                                [#] (0.005s) TestReplicaHTTPClients-caddy-1-2: logs

License

MIT

Documentation

Index

Constants

View Source
const (
	// TLSCACertPath is the in-container path to the world CA certificate.
	TLSCACertPath = "/tls/ca.crt"
	// TLSCertPath is the in-container path to the container's leaf certificate.
	TLSCertPath = "/tls/cert.pem"
	// TLSKeyPath is the in-container path to the container's private key.
	TLSKeyPath = "/tls/key.pem"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type ContainerSpec

type ContainerSpec struct {
	// Image is the container image to use (e.g., "alpine:latest")
	// If FromDockerfile is set, this is ignored.
	Image string

	// Isolated blocks internet access for this container. The container can
	// still communicate with other containers in the same testworld, but all
	// traffic to external networks is dropped.
	Isolated bool

	// Aliases adds extra DNS aliases for this container, making it reachable
	// by additional names from other containers in the world.
	Aliases []string

	// Subdomains adds extra DNS aliases by joining each subdomain with each
	// container name and alias using a dot. For example, Subdomains: ["foo"]
	// on a container named "bar" with Aliases: ["baz"] creates additional
	// aliases "foo.bar" and "foo.baz".
	Subdomains []string

	// FromDockerfile allows building an image from a Dockerfile.
	FromDockerfile testcontainers.FromDockerfile

	// Replicas is the number of identical containers to create.
	// When > 1, the WorldContainer represents a group of replicas.
	// Methods are executed on all replicas. The group name resolves
	// to all replica IPs via DNS round-robin.
	// Defaults to 1 if unset or zero.
	Replicas int

	// Entrypoint overrides the container's default entrypoint
	Entrypoint []string

	// KeepAlive keeps the container running indefinitely. When set and no Cmd
	// is provided, "sleep infinity" is used as the command.
	KeepAlive bool

	// Cmd is the command to run in the container
	Cmd []string

	// Env is a map of environment variables to set in the container
	Env map[string]string

	// ExposedPorts is a list of ports to expose (e.g., "80", "8080/tcp")
	ExposedPorts []string

	// Files is a list of files to copy into the container before it starts.
	Files []testcontainers.ContainerFile

	// Tmpfs is a map of tmpfs mounts (path -> options)
	Tmpfs map[string]string

	// ConfigModifier allows customizing the container config.
	ConfigModifier func(*container.Config)

	// HostConfigModifier allows customizing the Docker host config.
	HostConfigModifier func(*container.HostConfig)

	// WaitingFor is the strategy to wait for the container to be ready.
	WaitingFor wait.Strategy

	// Requires declares that this container depends on the listed containers
	// being ready before creation starts. If any dependency fails, this
	// container also fails without being created.
	Requires []WorldContainer

	// After is like Requires but only delays method calls (Await, Exec, etc.),
	// not container creation. The container is created in parallel with its
	// After dependencies, but any method blocks until they are ready.
	After []WorldContainer

	// OnDestroy is a callback function that is called before the container is terminated.
	OnDestroy func(WorldContainer)
}

ContainerSpec defines the specification for creating a container.

type Event

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

type World

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

world represents the environment in which a test runs. Containers added to the world share the same network and logs are collected in a common log file. The world is destroyed at the end of the test.

func New

func New(t *testing.T, logPath string) *World

New creates a new testworld. w.Destroy() should be deferred right after calling this function. If logPath is not empty, a world log will be created in the specified directory.

func (*World) AwaitAll added in v0.0.3

func (w *World) AwaitAll()

AwaitAll waits for all containers in the world to be ready.

func (*World) Destroy

func (w *World) Destroy()

Destroy cleans up the testworld.

func (*World) NewContainer

func (w *World) NewContainer(spec ContainerSpec) WorldContainer

NewContainer creates a new container and adds it to the World. Container creation happens in the background and WorldContainer methods wait for it to be ready. Call Await() to explicitly block until ready. Set spec.Replicas to create multiple identical containers as a group.

type WorldContainer

type WorldContainer struct {
	Name string
	// contains filtered or unexported fields
}

func (*WorldContainer) Await

func (wc *WorldContainer) Await()

Await blocks until all replica containers are created and started.

func (*WorldContainer) Exec

func (wc *WorldContainer) Exec(cmd []string, expectCode int)

Exec executes a command in all replica containers concurrently.

func (*WorldContainer) LogFile

func (wc *WorldContainer) LogFile(path string)

LogFile copies a file from all replica containers to the world log concurrently.

func (*WorldContainer) Wait

func (wc *WorldContainer) Wait(waitStrategy wait.Strategy)

Wait waits for all replica containers concurrently with a given wait strategy.

type WorldLog

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

func NewWorldLog

func NewWorldLog(world *World, path string) (*WorldLog, error)

Jump to

Keyboard shortcuts

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