Smarty
Let's build an xUnit-style test runner for Go!Why? What do you mean 'why'? Because we can! What's wrong with you?
Michael Whatcott
Michael Whatcott
 • 
July 2, 2018
Tags

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.


Source Code Download

Subscribe to our blog!
Learn more about RSS feeds here.
rss feed iconSubscribe Now
Read our recent posts
Smarty Launches US GeoReference Data, Providing the Easiest, Most Accurate API Needed To Access Census Tract and Block
Arrow Icon
PROVO, Utah, April 10, 2024 –&nbsp;Smarty, the address data intelligence leader, announces today the launch of US GeoReference Data, a set of updates to Smarty's US Address Enrichment solutions. US GeoReference Data is a cloud-native solution that will allow organizations to append the geographic data found in U. S. Census Block and Tract information into accurately geocoded addresses. &nbsp;Smarty's US GeoReference Data is the simplest and fastest way for organizations to access Census Blocks, Tracts, location names and statuses, as well as additional Census ID information relevant to a property.
International Be Kind to Lawyers Day
Arrow Icon
Lawyers get a bad rap. Lawyers have been around for a long time, and when you're this old, you're bound to collect your fair share of good and bad. It's true, ask your grandpa. We've got records of people described as "lawyers" going back to ancient Greece, Rome, and the Byzantines. These first individuals were folks who were asked to speak for the accused because those under scrutiny were—understandably so—shaken up by the situation. It went from someone who was your friend and did you a favor by speaking on your behalf to someone who knew all of the laws, and you'd hire them to speak eloquently for you.
Navigating the Future: Geocoding & Address Data Trends in the Cloud
Arrow Icon
In a recent webinar, Berk Charlton, Chief Product Officer at Smarty, provided an in-depth look at our industry-leading address intelligence suite designed to provide highly accurate geocoding and address validation. Here are the main points Berk covered during the session:Comprehensive Product Suite: Smarty offers a wide range of products, including&nbsp;address validation,&nbsp;rooftop geocoding,&nbsp;global address auto-complete, and&nbsp;address data enrichment. These tools are designed to handle various aspects of address management and enhancement in industries from&nbsp;Insurance to&nbsp;Healthcare to&nbsp;Telecom and more.
Ready to get started?