testworld

package module
v0.0.0-...-6ea6e40 Latest Latest
Warning

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

Go to latest
Published: Feb 13, 2026 License: MIT Imports: 14 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.
  • 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",
        Cmd:   []string{"sleep", "60"},
    })

    // Wait for all servers to be ready
    servers.Await()

    // The group name resolves to all 3 server IPs via DNS round-robin
    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,

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

    // Command to run
    Cmd: []string{"sleep", "30"},

    // 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"),

    // 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",
    Cmd:   []string{"sleep", "60"},
})

// Wait for all servers to be ready
servers.Await()

// The group name resolves to all 3 server IPs
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.

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: 5.896s):
ID  | Process Visualization
----|--------------------------------------------------------------------------------
000 |[########] (0.653s) World: Create
001 |        [####################] (1.523s) World: add alpine container TestReplicaHTTP-alpine-1
002 |        [######################] (1.674s) World: add caddy container TestReplicaHTTP-caddy-1-1
003 |        [######################] (1.682s) World: add caddy container TestReplicaHTTP-caddy-1-3
004 |        [###########################] (2.061s) World: add caddy container TestReplicaHTTP-caddy-1-2
005 |                                    [#] (0.106s) TestReplicaHTTP-alpine-1: exec sh -c test "$(nslookup TestReplicaHTTP-caddy-1 | grep 'Address' | grep -cv '127.0.0.11')" -eq 3
006 |                                      [#] (0.105s) TestReplicaHTTP-alpine-1: exec wget -q -O /dev/null http://TestReplicaHTTP-caddy-1-1:80/
007 |                                       [#] (0.103s) TestReplicaHTTP-alpine-1: exec wget -q -O /dev/null http://TestReplicaHTTP-caddy-1-2:80/
008 |                                         [#] (0.103s) TestReplicaHTTP-alpine-1: exec wget -q -O /dev/null http://TestReplicaHTTP-caddy-1-3:80/
009 |                                          [#####################################] (2.764s) World: destroy
010 |                                          [#] (0.005s) TestReplicaHTTP-caddy-1-1: logs
011 |                                                     [#] (0.014s) TestReplicaHTTP-caddy-1-2: logs
012 |                                                               [#] (0.003s) TestReplicaHTTP-caddy-1-3: logs
013 |                                                                          [#] (0.002s) TestReplicaHTTP-alpine-1: logs

License

MIT

Documentation

Index

Constants

This section is empty.

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

	// 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

	// 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

	// 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) 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 and writes the output to the world log.

func (*WorldContainer) LogFile

func (wc *WorldContainer) LogFile(path string)

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

func (*WorldContainer) Wait

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

Wait waits for all replica containers 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