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][practice-writing]. 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
andSmallSize
), 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
-
The 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. ↩