#Go | #TDD

July 9, 2018

Test-Driven Development in Go

In this video, Robert Martin uses Kotlin and JUnit to illustrate his Three Laws of TDD. But what about Go? Follow me and challenge the master! We will walk in his footsteps with the only help of Brad Fitzpatrick’s checkFunc pattern.

The Three Laws

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

Prime factors

Step 1: the API

We want a function to list the prime factors of a given number.

No clue how to solve the problem, but we might have a function signature already. Let’s start with this.

// factorsof.go
func factorsOf(n int) []int

Next, we have to define our checkFunc: a test function checker. Think of it as a matcher function that takes as arguments all the interesting values our factorsOf function produce.

In our case, we are only interested in the slice of integers that factorsOf returns. That will be the only argument of our checker type.

// factorsof_test.go
type checkFunc func([]int) error

Step 2: the first failing test

Let’s lay a table-driven test.

func TestFactorsOf(t *testing.T) {
	type checkFunc func([]int) error

	// SECTION 1: checkers
	isEmptyList := func(have []int) error {
		if len(have) > 0 {
			return fmt.Errorf("expected empty list, found %v.", have)
		}
		return nil
	}

	// SECTION 2: test cases
	tests := [...]struct {
		in    int
		check checkFunc
	}{
		// the first test: factors of 1
		{1, isEmptyList},
	}

	// SECTION 3: test logic
	for _, tc := range tests {
		t.Run(fmt.Sprintf("Factors of %d", tc.in), func(t *testing.T) {
			factors := factorsOf(tc.in)
			if err := tc.check(factors); err != nil {
				t.Error(err)
			}
		})
	}
}
  • isEmptyList is our first checker. It returns an error if the list is not empty.
  • tests is the array of cases we want to test. A test case in an unnamed struct that carries the input to feed the target function with; and a checkFunc for checking the return value.
  • The first case states that we expect that input 1 will produce an empty list.
  • In the last section, we iterate over tests and we spin a named subtest (t.Run) for each case.

The production code that is sufficient to pass the failing test will then be:

// factorsof1.go
func factorsOf(n int) []int {
	return []int{}
}

Remember: not more production code than is sufficient to pass the one failing unit test. Now this sentence has a visual meaning :)

Note that so far, that’s a ~10 / 1 LOC ratio between the test code and the production code. That’s fine. We are using a verbose pattern; it’ll get slightly better after a couple iterations.

Step 3: iterate

The prime factor of 2 is 2. First of all, we need a new checker to check a non-empty list. Here it is!

is := func(want ...int) checkFunc {
	return func(have []int) error {
		if !reflect.DeepEqual(have, want) {
			return fmt.Errorf("expected list %v, found %v.", want, have)
		}
		return nil
	}
}

It’s a closure: the actual matcher is a function that is dynamically generated based on the expected value that is passed to the parent function.

// factorsof2_test.go
func TestFactorsOf(t *testing.T) {
	type checkFunc func([]int) error

	// SECTION 1: checkers
	isEmptyList := func(have []int) error {
		if len(have) > 0 {
			return fmt.Errorf("Expected empty list, found %v.", have)
		}
		return nil
	}
	is := func(want ...int) checkFunc {
		return func(have []int) error {
			if !reflect.DeepEqual(have, want) {
				return fmt.Errorf("Expected list %v, found %v.", want, have)
			}
			return nil
		}
	}

	// SECTION 2: test cases
	tests := [...]struct {
		in    int
		check checkFunc
	}{
		{1, isEmptyList},
		{2, is(2)},
	}

	// SECTION 3: test logic
	for _, tc := range tests {
		t.Run(fmt.Sprintf("Factors of %d", tc.in), func(t *testing.T) {
			factors := factorsOf(tc.in)
			if err := tc.check(factors); err != nil {
				t.Error(err)
			}
		})
	}
}
// factorsof2.go
func factorsOf(n int) []int {
	var factors []int
	if n > 1 {
		factors = append(factors, 2)
	}
	return factors
}

Is this cheating? Sure not. We are asked for the minimal required code; there will be room for refactoring in the next steps.

{3, is(3)}

Now is the time to generalise a bit!

// factorsof3.go
func factorsOf(n int) []int {
	var factors []int
	if n > 1 {
		factors = append(factors, n)
	}
	return factors
}

That 2 we were appending to factors is now an n. This change involves a single character and it’s the most effective in the whole process.

{4, is(2, 2)}

OK let’s first check if the number is divisible by 2, and append that to the slice.

// factorsof4.go 
func factorsOf(n int) []int {
	var factors []int
	
	// return an empty slice if n <= 1
	if n > 1 {
		
		// factor 2
		if n%2 == 0 {
			factors = append(factors, 2)
			n /= 2
		}
		
		// append the remainder
		if n > 1 {
			factors = append(factors, n)
		}
	}
	
	return factors
}
{5, is(5)},
{6, is(2, 3)},
{7, is(7)},

Awesome, tests are still passing.

{8, is(2, 2, 2)},

Uh-oh. We never instructed our algorithm to output more than two factors.

// factorsof8.go
func factorsOf(n int) []int {
	var factors []int
	if n > 1 {
		
		// factor all the 2s
		for n%2 == 0 {
			factors = append(factors, 2)
			n /= 2
		}
		
		// append the remainder
		if n > 1 {
			factors = append(factors, n)
		}
	}
	return factors
}

Just by turning an if into a for, we now have a loop and our algorithm is not bound to return a finite number of factors anymore.

{9, is(3, 3)},

In order to be able to factor 3s, we could add a new for cycle for factoring threes.

This is maybe the most difficult part of this exercise: we have to take that decision. Is it time to refactor yet? This question has led to uncountable ticket explosions in the history of software engineering. The answer is often subjective. In this case, the decision is pretty straightforward:

// factorsof9.go
func factorsOf(n int) []int {
	var factors []int
	for divisor := 2; n > 1; divisor++ {
		for n%divisor == 0 {
			factors = append(factors, divisor)
			n /= divisor
		}
	}
	return factors
}

Here we go.

Apparently, test driven development is a way to incrementally derive solutions to problems.

As Mr. Martin puts it: we didn’t design the algorithm upfront. Where did it come from? We just made the test cases pass, one by one.

// factorsof9_test.go
package main

import (
	"fmt"
	"reflect"
	"testing"
)

func TestFactorsOf(t *testing.T) {
	type checkFunc func([]int) error

	// checkers
	isEmptyList := func(have []int) error {
		if len(have) > 0 {
			return fmt.Errorf("Expected empty list, found %v.", have)
		}
		return nil
	}
	is := func(want ...int) checkFunc {
		return func(have []int) error {
			if !reflect.DeepEqual(have, want) {
				return fmt.Errorf("Expected list %v, found %v.", want, have)
			}
			return nil
		}
	}

	// test cases
	for _,tc := range [...]struct {
		in    int
		check checkFunc
	}{
		{1, isEmptyList},
		{2, is(2)},
		{3, is(3)},
		{4, is(2, 2)},
		{5, is(5)},
		{6, is(2, 3)},
		{7, is(7)},
		{8, is(2, 2, 2)},
		{9, is(3, 3)},
		{2 * 2 * 3 * 3 * 5 * 7 * 11 * 11 * 13, is(2, 2, 3, 3, 5, 7, 11, 11, 13)},
	} {
		t.Run(fmt.Sprintf("Factors of %d", tc.in), func(t *testing.T) {
			factors := factorsOf(tc.in)
			if err := tc.check(factors); err != nil {
				t.Error(err)
			}
		})
	}
}

Have fun and write tests!