Mike Bland

Instigator

Go, Oh So Close to Perfect!

The recent Go 1.2 release contains genius test coverage features, but the announcement illustrating them contains an annoying flaw

- Boston
Tags: Go, Google, programming, technical
Discuss: Discuss "Go, Oh So Close to Perfect!" on Google+

Go version 1.2 was recently released, and it contains genius new test coverage features that almost make me regret not being a programmer anymore. Really. Go take a look for yourself, and tell me that built-in coverage tools that easily generate web-browsable results that not only show covered lines but express execution counts using a heat map color scheme—and incurring roughly only 3% runtime overhead—doesn’t bring a tear to your eye.1

Despite this, there is one tiny detail about the announcement itself which brings a small tear of sadness to my eye: The use of a data-driven test. I know, I know, I’m not even a programmer anymore, and who am I to nitpick Rob Pike’s test code, of all people! But please, trust me: I’m really not that religious about many things in life, much less programming, but aside from preferring composition to inheritance—always, and which Go has baked into the language itself, FTW!—data-driven tests are another sore spot.

Of course, part of this is compounded by the fact that there isn’t a Google Test-esque testing framework in the Go standard libraries. But we’re in luck, as Aaron Jacobs has provided ogletest to the world. Here’s the example from the Go test coverage announcement post rewritten using the ogletest package:

package size

import (
	. "github.com/jacobsa/ogletest"
	"testing"
)

// Hook into the Ogletest framework for 'go test'.
func TestOgletest(t *testing.T) { RunTests(t) }

// Structure defining a test suite, i.e. logical grouping of test
// functions, plus an initialization hook.
type SizeTest struct{}

func init() { RegisterTestSuite(&SizeTest{}) }

func (t *SizeTest) NegativeSize() {
	ExpectEq("negative", Size(-1))
}

func (t *SizeTest) SmallSize() {
	ExpectEq("small", Size(5))
}

Admittedly, pulling in a full-on testing framework would be a bit heavyweight for a small blog example. But when writing large quantities of “real” tests for production code, without resorting to excessive logical duplication or confusing and brittle data-driven methods, such a framework can prove absolutely essential. Frameworks such as these allow you:

  • to write numerous self-contained test functions that keep the intent of the test case, expressed in the function name (e.g. NegativeSize and SmallSize), and the test data itself closely aligned;
  • to share common setup code if necessary (i.e. a test fixture, as SizeTest would be if it weren’t empty);
  • and to avoid lumping every test case together in a flat table that requires mental overhead to decipher whenever a test is added, modified, or broken.

Plus, a test framework can provide a lot more detailed feedback, in a standardized format, which can provide a ton of fast-debugging value when things do break. While Go’s traditional terseness is admirable, there’s a lot to be said for output that’s easily greppable or vgreppable when something goes wrong. Here’s the above test running with no breakages:

$ go test 
[----------] Running tests from SizeTest
[ RUN      ] SizeTest.NegativeSize
[       OK ] SizeTest.NegativeSize
[ RUN      ] SizeTest.SmallSize
[       OK ] SizeTest.SmallSize
[----------] Finished with tests from SizeTest
PASS
ok      example/testing/coverage        0.014s

Here’s the same test after changing the original code such that NegativeSize() fails:

$ go test 
[----------] Running tests from SizeTest
[ RUN      ] SizeTest.NegativeSize
ogletest_test.go:17:
Expected: negative
Actual:   less than zero

[  FAILED  ] SizeTest.NegativeSize
[ RUN      ] SizeTest.SmallSize
[       OK ] SizeTest.SmallSize
[----------] Finished with tests from SizeTest
--- FAIL: TestOgletest (0.00 seconds)
FAIL
exit status 1
FAIL    example/testing/coverage        0.012s

Bear in mind that, using ogletest or some other framework, such output is standardized across all tests that use the framework. Each test doesn’t have to reinvent the wheel when it comes to error reporting, and each programmer doesn’t have to reverse-engineer the wheel when something fails. Just as having coding standards is a good practice to reduce mental friction when working with code, using test frameworks providing standardized output reduces mental friction when writing tests, reading tests, and deciphering breakage messages.

Of course there’s a lot more to ogletest and its companion packages, oglemock and oglematchers, but I just wanted to get the basic flavor out there while go test -cover is fresh in everyone’s minds—and to provide an alternative to the provided data-driven test example in Rob Pike’s otherwise brilliant blog post.

OK, the former programmer will now shut up and get back to his studies…

Footnotes

1The blog doesn’t mention what sort of memory overhead this feature incurs, or any other performance factors; but really, coverage is more useful when run on small tests, maybe medium tests, so I’d imagine that figure holds in day-to-day development practice without rubbing up against any practical machine limits.