Verify - Software Engineering

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?

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
}