Test-Driven Development in Go

With Robert Martin’s Three Laws of TDD

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

You are not allowed to write any production code unless it is to make a failing unit test pass. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 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.

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.

for checking the return value. The first case states that we expect that input 1 will produce an empty list.

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:

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.

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!

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.

{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.

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:

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.

Have fun and write tests!