sweet

package module
v0.0.0-...-f32bd8a Latest Latest
Warning

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

Go to latest
Published: Mar 4, 2024 License: MIT Imports: 1 Imported by: 0

README

Sweet; a simple test suite

Go Reference

sweet is a simple test suite 'framework'; if you can even call it a framework.

sweet supplies the most 'test suite' functionality possible, while staying close to standard golang testing idioms.

The focus is on setting up, sharing and reusing test dependencies. With the goal of minimising the risk of accidentally mutating shared state between runs.

There's no big up front work to implement it, and when you don't need it you can stick to testing.T as usual.

Use

See the package example for a quick intro. And other examples for details.

Reuse & skipping boilerplate

DepFactory functions are the interface that can be centralised, reused and shared.

For example common database or other external dependencies can have a set of stock test containers that can spin themselves up for every test and clean themselves after. All defined once and reused throughout every project. The same goes for fixture loaders, http Servers etc.

This is probably sweet's biggest power: a shared pattern for seperating test setup from the test itself. That pattern being a building block you can stack up and go higher with.

See:

Documentation

Overview

Example

A DepFactory creates and initialises all the structs and values your tests depend on. The software under test itself, but also mocks, databases that need spinning up etc. It takes a testing.T so it can `Cleanup` once the test is over.

sweet.Run takes a DepFactory and runs a test, just like testing.T.Run but your test function gets fresh data to test on every time.

package main

import (
	"testing"

	"github.com/barry-hennessy/test/sweet"
)

type flammable struct {
	onFire bool
}

func (f *flammable) Ignite() {
	f.onFire = true
}

func (f *flammable) Extinguish() {
	f.onFire = false
}

func main() {
	t := &testing.T{}

	// Your dependency factory creates a fresh set of what your tests need and
	// cleanup once the test is done.
	flammableFactory := func(t *testing.T) *flammable {
		f := &flammable{}

		// Make sure to put the fire out when we're done
		t.Cleanup(func() {
			f.Extinguish()
		})

		return f
	}

	// `sweet.Run` creates a new instance and passes it to your test function.
	sweet.Run(t, "it is on fire", flammableFactory, func(t *testing.T, f *flammable) {
		f.Ignite()
		// ...
	})

	// A fresh instance every time.
	sweet.Run(t, "fire spreads", flammableFactory, func(t *testing.T, f *flammable) {
		f.Ignite()
		// ...
	})
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run[deps any, ptrDeps *deps](
	t *testing.T,
	testName string,
	factory DepFactory[deps],
	coreTest func(t *testing.T, d deps),
) bool

Run runs a subtest, just like testing.T.Run, except it takes a DepFactory that generates a new set of test dependencies for each test. The test is passed the dependencies as it's second argument.

Compared to testing.T.Run:

t.Run("subtest name", func(t *testing.T) {...})
sweet.Run(t, "subtest name", func(t *testing.T) deps, func(t *testing.T, d deps) {...})
Example (Direct)

This is the most straightforward way to use `sweet`.

If your software under test is wrapped up in one factory function just pass it to sweet.Run and use your fresh value in every test.

package main

import (
	"testing"

	"github.com/barry-hennessy/test/sweet"
)

type flammable struct {
	onFire bool
}

func flammableFactory(t *testing.T) *flammable {
	f := &flammable{}

	t.Cleanup(func() {
		f.Extinguish()
	})

	return f
}

func (f *flammable) Ignite() {
	f.onFire = true
}

func (f *flammable) Extinguish() {
	f.onFire = false
}

func main() {
	t := &testing.T{}

	// flammableFactory returns exactly what we're testing
	sweet.Run(t, "flames are hot", flammableFactory, func(t *testing.T, f *flammable) {
	})

	sweet.Run(t, "flames are orange", flammableFactory, func(t *testing.T, f *flammable) {
	})
}
Output:

Example (Functional)

A balanced approach when your test needs multiple dependencies is to use a function that returns multiple values. With this you can avoid sweet.Run altogether.

So why not use the functional approach every time?

You absolutely can.

Remember, the main goal of sweet is to seperate out your test dependencies and provide (and share) reusable building blocks.

If your functional factories:

  • create clean test dependencies
  • clean up after themselves
  • can be composed with others

Then go for it!

package main

import (
	"testing"
)

type (
	engine interface {
		Rev()
	}

	truck interface {
		Vroom()
	}

	hose interface {
		IsReeledUp() bool
	}

	fireTruck struct {
		hose   hose
		engine engine
	}
	mockEngine struct{}
	mockHose   struct{}
)

func (_ mockEngine) Rev() {}

func (t fireTruck) Vroom() {
	t.engine.Rev()
}

func (h mockHose) IsReeledUp() bool {
	return true
}

func main() {
	t := &testing.T{}

	fireTruckFactory := func(t *testing.T) (truck, hose, engine) {
		mockHose := mockHose{}
		mockEngine := mockEngine{}

		return fireTruck{mockHose, mockEngine}, mockHose, mockEngine
	}

	t.Run("when it is on fire", func(t *testing.T) {
		truck, hose, _ := fireTruckFactory(t)
		truck.Vroom()

		if !hose.IsReeledUp() {
			t.Error("you can't be driving around with a dangling hose")
		}
	})
}
Output:

Example (MapOfDependencies)

If your test needs multiple dependencies and you want to avoid the boilerplate of creating a struct you can just use a map or slice.

It reduces the up front boilerplate but you need to cast where you use the values. Which can be a fine trade off if you only use the dependencies in one or two tests.

package main

import (
	"testing"

	"github.com/barry-hennessy/test/sweet"
)

type (
	engine interface {
		Rev()
	}

	truck interface {
		Vroom()
	}

	hose interface {
		IsReeledUp() bool
	}

	fireTruck struct {
		hose   hose
		engine engine
	}
	mockEngine struct{}
	mockHose   struct{}
)

func (_ mockEngine) Rev() {}

func (t fireTruck) Vroom() {
	t.engine.Rev()
}

func (h mockHose) IsReeledUp() bool {
	return true
}

func main() {
	t := &testing.T{}

	fireTruckFactory := func(t *testing.T) map[string]any {
		mockHose := mockHose{}
		mockEngine := mockEngine{}

		return map[string]any{
			"truck":  fireTruck{mockHose, mockEngine},
			"hose":   mockHose,
			"engine": mockEngine,
		}
	}

	sweet.Run(t, "when it is on fire", fireTruckFactory, func(t *testing.T, d map[string]any) {
		d["truck"].(truck).Vroom()

		if !d["hose"].(hose).IsReeledUp() {
			t.Error("you can't be driving around with a dangling hose")
		}
	})
}
Output:

Example (PitfallNesting)

Pitfall: Nesting sweet.Run calls

If you're used to using a different test suite you might be looking for `BeforeSuite` or `AfterSuite` functions that set up some state for all your tests and clean up at the end.

You can easily do this with sweet, it's just another level of sweet.Run calls. In fact you can nest and organise your test dependencies as much or as little as you like.

Just be aware that this undermines the _fresh dependencies_ that sweet tries to provide. The dependencies of the outer calls are shared between the inner calls.

package main

import (
	"testing"

	"github.com/barry-hennessy/test/sweet"
)

type (
	engine interface {
		Rev()
	}

	truck interface {
		Vroom()
	}

	hose interface {
		IsReeledUp() bool
	}

	fireTruck struct {
		hose   hose
		engine engine
	}
	mockEngine struct{}
	mockHose   struct{}
)

type flammable struct {
	onFire bool
}

func (f *flammable) Ignite() {
	f.onFire = true
}

func (f *flammable) Extinguish() {
	f.onFire = false
}

func (_ mockEngine) Rev() {}

func (t fireTruck) Vroom() {
	t.engine.Rev()
}

func (h mockHose) IsReeledUp() bool {
	return true
}

func main() {
	t := &testing.T{}

	flammableFactory := func(t *testing.T) *flammable {
		return &flammable{}
	}

	sweet.Run(t, "when it is on fire", flammableFactory, func(t *testing.T, f *flammable) {
		type fireTruckDeps struct {
			truck  truck
			hose   hose
			engine engine
		}

		fireTruckFactory := func(t *testing.T) fireTruckDeps {
			mockHose := mockHose{}
			mockEngine := mockEngine{}

			return fireTruckDeps{
				truck:  fireTruck{mockHose, mockEngine},
				hose:   mockHose,
				engine: mockEngine,
			}
		}

		// This is set for all tests within this block.
		// If any test calls `f.Extinguish` you have a race condition and flaky tests
		f.Ignite()

		sweet.Run(t, "the alarm goes off", fireTruckFactory, func(t *testing.T, d fireTruckDeps) {
			d.truck.Vroom()
			// ...
		})

		sweet.Run(t, "the fire brigade comes", fireTruckFactory, func(t *testing.T, d fireTruckDeps) {
			d.truck.Vroom()
			// ...
		})
	})
}
Output:

Example (PitfallNestingAlternative)

An alternative to `BeforeSuite`/`AfterSuite` that avoids accidental sharing of upper level dependencies.

Instead of nesting your sweet.Run calls, nesting your dependency factories can achieve the same effect; just with a fresh top level dependency.

package main

import (
	"testing"

	"github.com/barry-hennessy/test/sweet"
)

type (
	engine interface {
		Rev()
	}

	truck interface {
		Vroom()
	}

	hose interface {
		IsReeledUp() bool
	}

	fireTruck struct {
		hose   hose
		engine engine
	}
	mockEngine struct{}
	mockHose   struct{}
)

type flammable struct {
	onFire bool
}

func flammableFactory(t *testing.T) *flammable {
	f := &flammable{}

	t.Cleanup(func() {
		f.Extinguish()
	})

	return f
}

func (f *flammable) Ignite() {
	f.onFire = true
}

func (f *flammable) Extinguish() {
	f.onFire = false
}

func (_ mockEngine) Rev() {}

func (t fireTruck) Vroom() {
	t.engine.Rev()
}

func (h mockHose) IsReeledUp() bool {
	return true
}

// fireTruckDeps houses everything your tests need to test
// how a fire truck behaves.
type fireTruckDeps struct {
	truck  truck
	hose   hose
	engine engine
}

func main() {
	t := &testing.T{}

	fireTruckFactory := func(t *testing.T) fireTruckDeps {
		mockHose := mockHose{}
		mockEngine := mockEngine{}

		flammable := flammableFactory(t)
		flammable.Ignite()

		return fireTruckDeps{
			truck:  fireTruck{mockHose, mockEngine},
			hose:   mockHose,
			engine: mockEngine,
		}
	}

	t.Run("when it is on fire", func(t *testing.T) {
		sweet.Run(t, "the alarm goes off", fireTruckFactory, func(t *testing.T, d fireTruckDeps) {
			d.truck.Vroom()
			// ...
		})

		sweet.Run(t, "the fire brigade comes", fireTruckFactory, func(t *testing.T, d fireTruckDeps) {
			d.truck.Vroom()
			// ...
		})
	})
}
Output:

Example (StructOfDependencies)

A straightforward option for managing multiple dependencies is to create a struct to house your related dependencies.

It's more up front work, but everything is typed and it can pay off if you're using the struct often.

package main

import (
	"testing"

	"github.com/barry-hennessy/test/sweet"
)

type (
	engine interface {
		Rev()
	}

	truck interface {
		Vroom()
	}

	hose interface {
		IsReeledUp() bool
	}

	fireTruck struct {
		hose   hose
		engine engine
	}
	mockEngine struct{}
	mockHose   struct{}
)

func (_ mockEngine) Rev() {}

func (t fireTruck) Vroom() {
	t.engine.Rev()
}

func (h mockHose) IsReeledUp() bool {
	return true
}

// fireTruckDeps houses everything your tests need to test
// how a fire truck behaves.
type fireTruckDeps struct {
	truck  truck
	hose   hose
	engine engine
}

func main() {
	t := &testing.T{}

	fireTruckFactory := func(t *testing.T) fireTruckDeps {
		mockHose := mockHose{}
		mockEngine := mockEngine{}

		return fireTruckDeps{
			truck:  fireTruck{mockHose, mockEngine},
			hose:   mockHose,
			engine: mockEngine,
		}
	}

	sweet.Run(t, "when it is on fire", fireTruckFactory, func(t *testing.T, d fireTruckDeps) {
		d.truck.Vroom()

		if !d.hose.IsReeledUp() {
			t.Error("you can't be driving around with a dangling hose")
		}
	})
}
Output:

Types

type DepFactory

type DepFactory[deps any] func(t *testing.T) deps

DepFactory creates all dependencies needed for a test run.

It is responsible for cleaning up using testing.T.Cleanup. This goes for resources it creates, and for state changes made by the test to its dependencies.

Directories

Path Synopsis
factories
tc Module

Jump to

Keyboard shortcuts

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