Alternate design to simplify tests
Testing existing code you have several options to write sleek tests. Table driven or inlined test helpers work nicely. When writing new code however you have the option to choose a design that will be easier to verify. One go idiom is to return a value with an error. What if you didn't follow that idiom?
- what if you always used panics?
- what if you only returned a struct with an optional error field?
Don't let the idiom stop you from experimenting. While working with inline helpers I found that functions, which only return errors, resulted in simpler and more readable tests. Two assert functions are needed, one for checking for an error and the other for nil errors. Remember that tests should focus on verifying logic, not data. In this case the logic is binary, failed or not.
func assertOk(t *testing.T) assertFunc {
return func(err error, msg ...string) {
t.Helper()
if err != nil {
if len(msg) > 0 {
t.Error(strings.Join(msg, " ")+":", err)
return
}
t.Error(err)
}
}
}
func assertBad(t *testing.T) assertFunc {
return func(err error, msg ...string) {
t.Helper()
if err == nil {
if len(msg) > 0 {
t.Error(strings.Join(msg, " "), "should fail")
return
}
t.Error("should fail")
}
}
}
type assertFunc func(error, ...string)
The initial design of the function double follows the go idiom of returning a value with an error. Redesign the function to take the resulting argument and only return an error adds a few more lines to the function. We also added the check for nil result. The nil check may be left out or removed once you have your tests.
// double sets the result to the double of i if i is positive but
// never more than max int
func double(result *int, i int) error {
if result == nil {
return fmt.Errorf("double: result cannot be nil")
}
if i < 0 {
*result = 0
return fmt.Errorf("double: i must be positive")
}
n := i * 2
if n < i {
*result = MAX
return nil
}
*result = n
return nil
}
Let's use our new assert functions.
func Test_double(t *testing.T) {
var r int
ok := assertOk(t)
ok(double(&r, 1))
ok(double(&r, 3))
ok(double(&r, MAX))
_k := assertBad(t)
_k(double(&r, -2))
_k(double(nil, 2))
// verify data, some is good
check := func(i, exp int) {
t.Helper()
var got int
double(&got, i)
if got != exp {
t.Errorf("got %v, exp %v", got, exp)
}
}
check(0, 0) // edge
check(1, 2) // ok
check(MAX, MAX) // other edge
check(-1, 0) // bad
}