Table-driven Tests in Go
For a Java programmer, transitioning to Go can evoke "Where's My JUnit?" Fortunately Go has both a built-in testing library and a very smooth way to write tests idiomatically.
Join the DZone community and get the full member experience.
Join For FreeI've started working in Go both professionally and personally and have enjoyed the experience. One reason is that Go takes a quite different approach to Java in some areas, which makes switching between the two a mind-expanding experience.
One such area is in unit testing. Go puts enough value on unit tests to make testing a part of the standard library (including code coverage, which I intend to discuss in the future). But the approach is different from a library like JUnit in that there are no assertions. Instead there are just functions like Errorf
to fail the test with a log message.
To see the full example from this article, see this small GitHub repository. It contains a function that returns the numeric value for a Roman numeral in string form.
Unit tests in Go are stored in files that end in _test.go
. These files will not be built with the normal code, but will be inspected for unit tests. In these files, we write functions starting with Test
that take a single parameter, of type *testing.T
. For example:
import "testing"
...
func TestValid(t *testing.T) {
// Run tests
}
The function does not need a return type; if the function ends as expected, the test passes. To fail the test, we use functions on the *testing.T
type. As mentioned above, this type does not provide "assertion" style functions; instead there are functions that log errors and fail the test. For this reason, Go tests use regular if/else expressions, such as:
if err != nil {
t.Errorf("Unexpected error for input %v: %v", tt.input, err)
}
This is more verbose than an assertion, but it has the advantage of being more explicit. The code is comprehensible to anyone who understands the language, and doesn't require learning a separate library. It also aligns test code with regular Go code, since checking for things like non-nil errors is a standard practice in Go (in place of the exception handling seen in other languages). For more information about Go's built-in support for unit testing, see this article, also on DZone.
To make up for the extra verbosity in checking for errors, Go encourages table driven testing. The idea is to build a data structure, usually a slice, that contains test inputs and expected outputs, then to iterate over the slice, testing each case.
While this kind of table-driven test is easy to do in other languages, such as Java, it is made very simple in Go by the ability to declare and populate data structures in a single statement.
For example, to test a function that takes a string and returns an int, we can declare the following slice:
var validTests = []struct {
input string
expected int
}{
{"", 0},
{"I", 1},
}
The equivalent in Java would probably be done by declaring a class, then creating a static initializer to build a collection of instances of that class. Not difficult, but more verbose.
The code to iterate over the table can be basic; of course, it needs to be tailored to reflect how the code under test should be initialized and called.
for _, tt := range validTests {
res, err := RomanToInt(tt.input)
if err != nil {
t.Errorf("Unexpected error for input %v: %v", tt.input, err)
}
if res != tt.expected {
t.Errorf("Unexpected value for input %v: %v", tt.input, res)
}
}
This code takes advantage of Go's simple iteration over slices. The use of for _, tt
allows us to ignore the index of each item and just use the item itself.
The best part about table-driven tests is that, once the test is written, we can ignore the test method and just add cases to the table. Also, the input and expected output are easily visible and associated together in the test file. The downside is that we have to be careful crafting error messages to make sure that it is clear exactly which test case is failing. It is also a bit more difficult to create breakpoints on specific test cases.
So far I've just shown a table-driven test for valid inputs and outputs. In a future article I'll show a table-driven test to perform error checking as well.
Opinions expressed by DZone contributors are their own.
Trending
-
SRE vs. DevOps
-
How to Use an Anti-Corruption Layer Pattern for Improved Microservices Communication
-
How To Use Git Cherry-Pick to Apply Selected Commits
-
Deploying Smart Contract on Ethereum Blockchain
Comments