Let's build an xUnit-style test runner for Go!
Writing test functions in Go is easy:
package stuff
import "testing"
func TestStuff(t *testing.T) {
t.Log("Hello, World!")
}
Running test functions is also easy:
$ go test -v
=== RUN TestStuff
--- PASS: TestStuff (0.00s)
stuff_test.go:6: Hello, World!
PASS
ok github.com/smartystreets/stuff 0.006s
Preparing shared state for multiple test functions is problematic. The usual recommendation is to use table-drive tests. But this approach has its limits. For us, xUnit is the ideal solution. It's simple, lightweight, and flexible. Wouldn't it be nice if we could define test methods on struct
types and leverage common xUnit conventions like setups/teardowns, skipped tests, etc..? I'm thinking along these imaginary lines:
package stuff
import "testing"
// Define fields to manage system-under-test here (the fixture state).
type TestCase struct {
*testing.T // Embedding *testing.T seems like a good idea for defining a test suite.
sut *SystemUnderTest
}
// Perform setup actions here (instantiate test fixture state).
func (t *TestCase) Setup() {
t.sut = NewSystemUnderTest()
}
func (t *TestCase) Test42() {
if result := t.sut.Computation(42); result != 42 {
t.Errorf("Got: [%d] Want: [%d]", result, 42)
}
}
func (t *TestCase) Test43() {
if result := t.sut.Computation(43); result != 43 {
t.Errorf("Got: [%d] Want: [%d]", result, 43)
}
}
The only problem is that the go test
tool expects top-level functions, not methods on a struct
type. And that's not going to change.
$ go test -v
testing: warning: no tests to run
PASS
ok github.com/smartystreets/stuff 0.006s
So, we need a way to connect a test function to methods on a struct
type. And ideally, we could instantiate new instances of that type (with freshly initialized state) for each test method. Maybe a variation that leverages subtests would be closer to reality?
package stuff
import "testing"
func TestStuff(t *testing.T) {
t.Run("Test42", new(TestCase).Test42)
t.Run("Test43", new(TestCase).Test43)
}
// Define fields to manage system-under-test here (the fixture state).
type TestCase struct {
sut *SystemUnderTest
}
// Perform setup actions here (instantiate test fixture state).
func (test *TestCase) Setup() {
test.sut = NewSystemUnderTest()
}
func (test *TestCase) Test42(t *testing.T) {
test.Setup()
if result := test.sut.Computation(42); result != 42 {
t.Errorf("Got: [%d] Want: [%d]", result, 42)
}
}
func (test *TestCase) Test43(t *testing.T) {
test.Setup()
if result := test.sut.Computation(43); result != 43 {
t.Errorf("Got: [%d] Want: [%d]", result, 43)
}
}
That was certainly more effective:
$ go test -v
=== RUN TestStuff
=== RUN TestStuff/Test42
=== RUN TestStuff/Test43
--- PASS: TestStuff (0.00s)
--- PASS: TestStuff/Test42 (0.00s)
--- PASS: TestStuff/Test43 (0.00s)
PASS
ok github.com/smartystreets/stuff 0.006s
But there are problems with this approach. Every time we define a new test method on the TestCase
type we have to remember to register a subtest in the top-level test function. Oh, and did you notice how each test was calling the Setup
method directly? This is something that should happen automatically if we're going to call this an xUnit-style test runner. It would be great if we could just call a method that points to our TestCase
and iterates all test methods, calling Setup
followed by a call to the test method.
From the calling side it could look something like this:
func TestStuff(t *testing.T) {
xunit.RunTests(new(TestCase), t)
}
Notice we have to provide the *testing.T
and an instance of our TestCase. The behavior defined in the mysterious xunit
package would then find all the tests and run them. Impossible, you say? Not so! In fact, a draft implementation is trivial!
package xunit
import (
"reflect"
"strings"
"testing"
)
func RunTests(fixture interface{}, t *testing.T) {
fixtureType := reflect.TypeOf(fixture)
for x := 0; x < fixtureType.NumMethod(); x++ {
testMethodName := fixtureType.Method(x).Name
if strings.HasPrefix(testMethodName, "Test") {
// IMPORTANT: each test gets a new instance!
instance := reflect.New(fixtureType.Elem())
setupMethod := instance.MethodByName("Setup")
callableSetup := setupMethod.Interface().(func())
callableSetup()
testMethod := instance.MethodByName(testMethodName)
callableTest := testMethod.Interface().(func(t *testing.T))
t.Run(testMethodName, callableTest)
}
}
}
This implementation makes a LOT of assumptions, lacks several features (like 'teardowns' and skipped tests) and isn't very robust, but hopefully you can see the emergence of an xUnit-style test runner. Most importantly, the tests are passing again:
$ go test -v
=== RUN TestStuff
=== RUN TestStuff/Test42
=== RUN TestStuff/Test43
--- PASS: TestStuff (0.00s)
--- PASS: TestStuff/Test42 (0.00s)
--- PASS: TestStuff/Test43 (0.00s)
PASS
ok github.com/smartystreets/stuff 0.006s
Congratulations, you now possess a basic understanding of the inner workings of gunit! Stay tuned for a future post featuring a more in-depth look into xUnit-style testing in Go with gunit. In the meantime, feel free to kick the tires and fix things up a bit.