kpat.io
June 8, 2019

TDD with Go

This post explains the basics of Test Driven Developmen (TDD) in the Go language. We’ll start with the basics of TDD and then cover a simple example with the factorial function.

The Basics of TDD

The core idea of TDD is to write tests prior to writing code. In contrast to Test First Development, TDD is an iterative process, which focusses on one test at a time. There are 3 steps to each cycle:

  • Red - Add a new test for which the code doesn’t exist yet. Obviously running the test after this phase will result in a failing test.
  • Green - Add the minimum amount of code to satisfy the test. At this point all tests should succeed.
  • Refactor - Improve the code without adding functionality

The cycle is also known as red-green-refactor. After each of these steps, the tests are executed again. This results in good code coverage and incrementally tested code, thus arguably better code quality.

An Example

Let’s have a look at a concrete example. Mathematical functions are a simple way of getting to know the concept of testing, since they can be modelled without including any dependencies. We pick the factorial function, wich is most commonly denoted by n!. The factorial is a mathematical function which takes the product of all positive integers up to the specified parameter n. Let’s look at a few examples

# Definition of factorial
$ n! = n * (n - 1) * (n - 2) * ... * 2 * 1
# Alternative definition
$ n! = n * (n - 1)!
# 0 is a special value
$ 0! = 1
$ 1! = 1
$ 2! = 2 * 1 = 1
$ 3! = 3 * 2 * 1 = 6
$ ...
$ 10! = 10 * 9 * 8 * ... * 2 * 1 = 3'628'800

Without further ado we setup our main.go, which looks as follows

// file: main.go

package main

func main() {
    // Insert code
}

And our test file main_test.go

// file: main_test.go

package main

// Empty for now

Like obedient TDD developers we’ll start with a test for our factorial function. 0! = 1 is a special value of this function. Let’s start there

// file: main_test.go

package main

import (
    "testing"
)

func TestZero(t *testing.T) {
    res := factorial(0)

    if 1 != res {
        t.Errorf("Expected 0! to be 1, got %d", res)
    }
}

With TDD in mind, we run the tests which returns the anticipated output, an error

$ go test
./main_test.go:8: undefined: factorial
FAIL    test [build failed]

Right now we’re in the red phase. Our tests are failing. The next step is to add as little functionality as possible to make our tests pass. We adapt out main.go as follows

// file: main.go
// ...

func factorial(num int) int {
    return 0
}

Now the code compiles and we don’t get that compiler error anymore. Yet, our software doesn’t work as expected yet.

$ go test
--- FAIL: TestZero (0.00s)
main_test.go:10: Expected 0! to be 1, got 0
FAIL
exit status 1
FAIL    test      0.006s

This step is crucial to TDD, since it assures us that we aren’t shooting shoot blanks. In other words, our test actually checks the functionality instead of giving us false positives. Let’s fix that really quick

// file: main.go
// ...

func factorial(num int) int {
    return 1
}

Now we run our test again and see green.

$ go test
PASS
ok      test      0.006s

The code looks quite simple and neat, hence we won’t go into the refactor step for now. Phew! Let’s keep the momentum and move to the next value. 1! = 1 already works - following the protocol, we first make it fail

// file: main_test.go
// ...

func TestOne(t *testing.T) {
    res := factorial(1)
    if 0 != res {
        t.Errorf("Expected 0! to be 1, got %d", res)
    }
}

And fix it again

// file: main_test.go
// ...

func TestOne(t *testing.T) {
    res := factorial(1)
    if 1 != res {
        t.Errorf("Expected 0! to be 1, got %d", res)
    }
}

For the next value we’ll add a more generic test case

// file: main_test.go
// ...

func TestN(t *testing.T) {
    var prov = []struct {
        n   int
        exp int
    }{
        {2, 2},
    }

    for _, tt := range prov {
        act := factorial(tt.n)

        if act != tt.exp {
            t.Errorf("Expected %d! to be %d, got %d", tt.n, tt.exp, act)
        }
    }
}

Which fails. Now the corresponding functionality

// file: main.go

func factorial(num int) int {
    if num <= 1 {
        return 1
    }

    return 2
}

Fine, we’ll stop with the constants now. Let’s implement our function recursively as in the second definition n! = n * (n - 1)!. We can break the current implementation with the test for n = 3. The test can now be extended as such

// file: main_test.go
// ...

func TestN(t *testing.T) {
    var prov = []struct {
        n   int
        exp int
    }{
        {2, 2},
        {3, 6}, // We added this
    }
    for _, tt := range prov {
        act := factorial(tt.n)

        if act != tt.exp {
            t.Errorf("Expected %d! to be %d, got %d", tt.n, tt.exp, act)
        }
    }
}

Red. Now for the actual code

// file: main.go
func factorial(num int) int {
    if num &lt;= 1 {
        return 1
    }

    return num * factorial(num-1)
}

And so on. I think you get the point

Conclusion

TDD enables developers to achieve a high test coverage quickly and be more aware of what testable code looks like. It’s a great way for getting used to testing.