github.com/go-tstr/tstr
go get github.com/go-tstr/tstr@v0.2.4
README
tstr: your ultimate testing library
tstr allows you to write application, integration and black-box tests like normal unit tests in Go.
With tstr you can easily declare the test dependencies like:
- compose files
- single containers
- cli commands
- main package of Go program
and let tstr take care of the rest.
Table of Contents
Usage
This library is build on top of two concepts:
- tstr.Tester
- tstr.Dependency
tstr.Tester
There's two common ways to use tester, either from func TestMain or from func TestXXX. For both of these approaches there's helper function; tstr.RunMain and tstr.Run, which make it easy to setup and run tstr.Tester.
tstr.RunMain
func TestMain(m *testing.M) {
tstr.RunMain(m, tstr.WithDeps(
// Pass test dependencies here.
))
}
With TestMain approach you will have single test env within the packge.
tstr.RunMain will setup the test env you defined, call m.Run(), cleanup test env and finally call os.Exit with returned exit code.
tstr.Run
This approach allows more granular control over test env. For example you can have single test env for each top level test. This can be usefull when you want to avoid any side effects and shared state between tests. Also this approach allows more advaced usage like creating a pool of test envs for parallel testing.
tstr.WithFn
Simplest way to use tstr.Run is with the tstr.WithFn option:
func TestMyFunc(t *testing.T) {
err := tstr.Run(
tstr.WithDeps(
// Pass test dependencies here.
),
tstr.WithFn(func() {
const (
input = 1
expected = 1
)
got := MyFunc(input)
assert.Equal(t, expected, got)
}),
)
require.NoError(t, err)
}
tstr.WithTable
For table driven tests you can use tstr.WithTable which loops over the given test table and executes test function for each element using t.Run:
func TestMyFunc(t *testing.T) {
type test struct {
Name string
input int
expected int
}
tests := []test{
{Name: "test-1", input: 1, expected: 1},
{Name: "test-2", input: 2, expected: 2},
{Name: "test-3", input: 3, expected: 3},
}
err := tstr.Run(
tstr.WithDeps(
// Add dependencies here.
),
tstr.WithTable(t, tests, func(t *testing.T, tt test) {
got := MyFunc(tt.input)
assert.Equal(t, tt.expected, got)
}),
)
require.NoError(t, err)
}
tstr.Dependency
tstr.Dependency declares an interface for test dependency which can be then controlled by tstr.Tester. This repo provides the most commonly used dependecies that user can use within their tests. Since tstr.Dependency is just an interface users can also implement their own custom dependencies.
Compose
Compose dependecy allows you to define and manage Docker Compose stacks as test dependencies. You can create a Compose stack from projects compose file and control its lifecycle within your tests.
func TestMain(m *testing.M) {
tstr.RunMain(m, tstr.WithDeps(
compose.New(
compose.WithFile("../docker-compose.yaml"),
compose.WithUpOptions(tc.Wait(true)),
compose.WithDownOptions(tc.RemoveVolumes(true)),
compose.WithEnv(map[string]string{"DB_PORT": "5432"}),
compose.WithWaitForService("postgres", wait.ForLog("ready to accept connections")),
),
))
}
Container
Container dependecy allows you to define and manage single containers as test dependencies. You can use predefined modules from testcontainer-go or create generic container.
func TestMain(m *testing.M) {
tstr.RunMain(m, tstr.WithDeps(
container.New(
container.WithModule(postgres.Run, "postgres:16-alpine",
postgres.WithDatabase("test"),
postgres.WithUsername("user"),
postgres.WithPassword("password"),
),
),
))
}
Cmd
Cmd dependecy is the most versatile dependency. It can be used for running any binary or even compiling a Go application and running it as dependency.
cmd.WithCommand
cmd.WithCommand allows using binaries from
func TestMain(m *testing.M) {
tstr.RunMain(m, tstr.WithDeps(
cmd.New(
cmd.WithCommand("my-app", "--listen", ":8080"),
cmd.WithWaitMatchingLine("Server ready"),
),
))
}
cmd.WithGoCode
This example compiles my-app Go application, instruments it for coverage collections, waits for it to be ready and finally starts running tests.
func TestMain(m *testing.M) {
tstr.RunMain(m, tstr.WithDeps(
cmd.New(
cmd.WithGoCode("../", "./cmd/my-app"),
cmd.WithReadyHTTP("http://localhost:8080/ready"),
cmd.WithEnvAppend("GOCOVERDIR=./cover"),
),
))
}
Custom Dependencies
You can also create your own custom dependencies by implementing the tstr.Dependency interface.
type Custom struct{}
func New() *Custom {
return &Custom{}
}
func (c *Custom) Start() error { return nil }
func (c *Custom) Ready() error { return nil }
func (c *Custom) Stop() error { return nil }
Acknowledgements
This library is based on the work originally done as part of (https://github.com/elisasre/go-common)[https://github.com/elisasre/go-common] and was extracted to it's own repo to be more approachable by users.
Overview
Package tstr provides testing framework for Go programs that simplifies testing with test dependencies.
Constants
const (
ErrStartFailed = strerr.Error("failed to start test dependencies")
ErrStopFailed = strerr.Error("failed to stop test dependencies")
)const (
ErrMissingTestFn = strerr.Error("missing Opt for test function")
ErrOverwritingTestFn = strerr.Error("trying to overwrite test function")
ErrMissingNameField = strerr.Error("missing field Name in test case struct")
ErrWrongTestCaseType = strerr.Error("wrong test case type")
)Functions
func Run
func Run(opts ...Opt) errorRun runs the test with the given options. Options are applied in the order they are passed. One of the options must provide the test function.
Options that provide the test function:
- WithM
- WithFn
- WithTable
func RunMain
func RunMain(m TestingM, opts ...Opt)RunMain is a convinience wrapper around Run that can be used inside TestMain. RunMain applies automatically WithM option which calls m.Run. Also os.Exit is called with non-zero exit code if Run returns any error.
Example:
func TestMain(m *testing.M) {
tstr.RunMain(m, tstr.WithDeps(MyDependency()))
}
Types
type Dependency
type Dependency interface {
Startable
Stoppable
}type ExitError
type ExitError intfunc (ExitError) Error
func (e ExitError) Error() stringtype Opt
type Opt func(*Tester) errorfunc WithDeps
func WithDeps(deps ...Dependency) OptWithDeps adds dependencies to the tester.
func WithFn
func WithFn(fn func()) OptWithFn uses the given function as the test function.
func WithM
func WithM(m TestingM) OptWithM uses the given testing.M as the test function.
func WithTable
func WithTable[T any](tt TestingT, cases []T, test func(*testing.T, T)) OptWithTable runs the given test function for each test case in the table.
type Runner
type Runner struct {
// contains filtered or unexported fields
}func NewRunner
func NewRunner(rr ...Dependency) *RunnerNewRunner creates a new Runner with the given dependencies. The dependencies will be started in the order they are provided and stopped in reverse order. If any of the dependencies fail to start, the rest of the dependencies will not be started. If any of the dependencies fail to stop, the rest of the dependencies will still be stopped. Start method should be non-blocking and return immediately after starting the dependency. Ready method should block until the dependency is ready to be used. Stop method should block until the dependency is stopped.
func (*Runner) Start
func (t *Runner) Start() errorStart starts the dependencies in the order they were provided and waits for them to be ready. Next dependency will not be started if the previous one fails to start or become ready. Stop should be always called after Start, even if Start fails. That way all the started dependencies will be stopped.
func (*Runner) Stop
func (t *Runner) Stop() errorStop stops all started dependencies in the reverse order they were started.
type Startable
type Startable interface {
Start() error
Ready() error
}type Stoppable
type Stoppable interface {
Stop() error
}type Tester
type Tester struct {
// contains filtered or unexported fields
}func NewTester
func NewTester(opts ...Opt) *TesterNewTester creates a new Tester with the given options. In most cases you should use Run function instead of creating Tester manually, Using NewTester can be useful if you need more control over the test execution or if you want to reuse same Tester instance.
func (*Tester) Init
func (t *Tester) Init() errorInit applies all options to the Tester.
func (*Tester) Run
func (t *Tester) Run() errorRun starts the test dependencies, executes the test function and finally stops the dependencies.
type TestingM
type TestingM interface {
Run() int
}TestingM contains required methods from *testing.M.
type TestingT
type TestingT interface {
Run(name string, fn func(*testing.T)) bool
}TestingT contains required methods from *testing.T.