containertest

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Feb 14, 2024 License: Apache-2.0 Imports: 13 Imported by: 0

README

containertest

Introduction

This is a Go library for starting an ephemeral Docker container. It is used to test integration between Go code and services such as PostgreSQL.

This is not an official Google product.

How to use it

For some containers such as DBs, startup can take a while. You may want to reuse the same container and USE separate databases/namespaces or separate tables to isolate your tests from each other.

For expensive containers, Start() from your TestMain() function, If you're not familiar with Go's TestMain() mechanism for global test initialization, see the docs: https://pkg.go.dev/testing#hdr-Main.

Cheaper containers which don't need to be shared can also be started within a test.

MySQL will be used as an example service, though any Service implementation could be used. Currently, two implementations of containertest.Service exist in this repo, stored in mysql.go and postgres.go.

package mypackage

import (
    "database/sql"
    "fmt"
    "io"
    "net"
    "os"
    "testing"

    "github.com/abcxyz/containertest"
    _ "github.com/go-sql-driver/mysql" // Link with the Go MySQL driver
)

var ci *containertest.ConnInfo
var mySQLService *containertest.MySQL

// TestMain runs once at startup to do test initialization common to all tests.
func TestMain(m *testing.M) {
	// Runs unit tests
	os.Exit(func() int {
		mySQLService = &containertest.MySQL{Version: "5.7"}
        var err error // := assignment on next line would shadow ci global variable
		ci, err = containertest.Start(mySQLService) // Start the docker container. Can also pass options.
		if err != nil {
			panic(fmt.Errorf("could not start mysql service: %w", err))
    }
		defer ci.Close()

		return m.Run()
	}())
}

func TestFoo(t *testing.T) {
	t.Parallel()
	// One thing you might want to do is create an SQL driver:

	m := mySQLService
	// Find the port docker exposed for your container
	mySQLPort := ci.PortMapper(m.Port())
	uri := fmt.Sprintf("%s:%s@tcp(%s)/%s", m.Username(), m.Password(),
		net.JoinHostPort(ci.Host, mySQLPort), "")
	db, err := sql.Open("mysql", uri)
	if err != nil {
		t.Fatal(err)
	}

	// application logic goes here
	_ = db
}

A note on leaked Docker containers and timeouts

The io.Closer returned from MustStart() will terminate the Docker container, which is good, so it won't sit around wasting resources after the test is done. There is another level of protection against leaked containers, though: the container will terminate itself after a configurable timeout (default 10 minutes). If your tests take longer than this, you might need to extend the timeout by passing WithTimeout(...) to MustStart().

Warning on GitHub Actions

This library doesn't currently work when the Go tests are themselves running inside a Docker container. It can successfully start a new container, but networking between the two containers doesn't work. This can cause problems if you configure GitHub Actions in a certain way.

Background: a GitHub Actions workflow runs inside a VM. A workflow consists of multiple steps. Each step can run in the base VM, or it can run inside a Docker container, for isolation. We recommend running the Go tests inside the VM, not inside a Docker container.

So, in your GitHub Actions YAML file, do this:

jobs:
  test:
    name: 'Go Test'
    runs-on: 'ubuntu-latest'
    steps:
    - uses: 'actions/setup-go@v5'

... and do not do this:

jobs:
  test:
    name: 'Go Test'
    runs-on: 'ubuntu-latest'
    container: 'golang:1.21'  # DO NOT DO THIS

Documentation

Overview

Package containertest provides an ephemeral container (such as a database) for integration testing. It's designed to be used in code that needs to work inside and outside google.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ConnInfo

type ConnInfo struct {
	Host string

	// PortMapper maps from container port to host port. Do not use after container is closed.
	PortMapper func(containerPort string) (hostPort string)
	// contains filtered or unexported fields
}

ConnInfo specifies how connect to the created container.

func Start

func Start(service Service, opts ...Option) (*ConnInfo, error)

Start starts a container, or returns an error. On err ConnInfo will be automatically closed and nil will be returned.

func (ConnInfo) Close

func (c ConnInfo) Close() error

Close implements io.Closer by passing to internal closer field.

type MySQL

type MySQL struct {
	Version string
}

MySQL satisfies Service, defining a MySQL server container.

func (*MySQL) Environment

func (m *MySQL) Environment() []string

Environment satisfies [Service.Environment].

func (*MySQL) ImageRepository

func (m *MySQL) ImageRepository() string

ImageRepository satisfies [Service.ImageRepository].

func (*MySQL) ImageTag

func (m *MySQL) ImageTag() string

ImageTag satisfies [Service.ImageTag].

func (*MySQL) Password

func (m *MySQL) Password() string

Password returns the password for the MySQL database.

func (*MySQL) Port

func (m *MySQL) Port() string

Port returns the internal port the MySQL container exposes.

func (*MySQL) StartupPorts

func (m *MySQL) StartupPorts() []string

StartupPorts satisfies [Service.StartupPorts].

func (*MySQL) TestConn

func (m *MySQL) TestConn(progressLogger TestLogger, connInfo *ConnInfo) error

TestConn satisfies [Service.TestConn].

func (*MySQL) Username

func (m *MySQL) Username() string

Username returns the username for the MySQL database.

type Option

type Option func(*config) *config

Option sets a configuration option for this package. Users should not implement these functions, they should use one of the With* functions.

func WithKillAfterSeconds

func WithKillAfterSeconds(seconds int) Option

WithKillAfterSeconds is an option that overrides the default time period after which the docker container will kill itself.

Containers might bypass the normal clean shutdown logic if the test terminates abnormally, such as when ctrl-C is pressed during a test. Therefore we instruct the container to kill itself after a while. The duration must be longer than longest test that uses the container. There's no harm in leaving lots of extra time.

func WithLogger

func WithLogger(l TestLogger) Option

WithLogger overrides the default logger. This logger will receive messages about service startup progress. The default is to use the go "log" package.

type Postgres

type Postgres struct {
	// Version is the ImageTag that will be returned by the Service interface.
	Version string
}

Postgres satisfies Service, defining a Postgres server container.

func (*Postgres) Environment

func (p *Postgres) Environment() []string

Environment satisfies [Service.Environment].

func (*Postgres) ImageRepository

func (p *Postgres) ImageRepository() string

ImageRepository satisfies [Service.ImageRepository].

func (*Postgres) ImageTag

func (p *Postgres) ImageTag() string

ImageTag satisfies [Service.ImageTag].

func (*Postgres) Password

func (p *Postgres) Password() string

Password returns the password for the Postgres database.

func (*Postgres) Port

func (p *Postgres) Port() string

Port returns the internal port the Postgres container exposes.

func (*Postgres) StartupPorts

func (p *Postgres) StartupPorts() []string

StartupPorts satisfies [Service.StartupPorts].

func (*Postgres) TestConn

func (p *Postgres) TestConn(progressLogger TestLogger, connInfo *ConnInfo) error

TestConn satisfies [Service.TestConn].

func (*Postgres) Username

func (p *Postgres) Username() string

Username returns the username for the Postgres database.

type Service

type Service interface {
	// ImageRepository returns the repository for docker image (ex: mysql).
	ImageRepository() string

	// ImageTag returns the tag for docker image (ex: 5.3).
	ImageTag() string

	// Environment returns variables to be set in container. Each element is in format of "KEY=VALUE".
	Environment() []string

	// StartupPorts is the list of ports that must be exposed by container before TestConn is run.
	StartupPorts() []string

	// TestConn takes a logger and a struct with connection info, and returns nil if app has started.
	TestConn(progressLogger TestLogger, info *ConnInfo) error
}

Service provides information about what container image should be started and how to know when it has finished stating up.

type TestLogger

type TestLogger interface {
	Log(args ...any)
	Logf(format string, args ...any)
}

TestLogger allows the caller to optionally provide a custom logger for printing status updates about service startup progress. The default is to use the go "log" package. testing.TB satisfies TestLogger, and is usually what you want to put here.

Jump to

Keyboard shortcuts

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