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
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.
SmallSize), and the test data itself closely aligned;
- to share common setup code if necessary (i.e. a test fixture, as
SizeTestwould 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
$ 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…
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.