Test-driven development with Go

Test-driven development with Go

calculator.png

The tests are not the thing. They’re the thing that gets us to the thing.

Welcome aboard! It's your first day as a Go developer at Texio Instronics, a major manufacturer of widgets and what-nots, and you've been assigned to the Future Projects division to work on a very exciting new product: a Go-powered electronic calculator. Let's get started.

Test-driven development

First, a word about the process. There's a style of programming (that isn't unique to Go, but is very popular with Go developers) called Test-Driven Development (TDD).

What this means is that before you write a program to do something (multiply two numbers, let's say), you first of all write a test.

A test is also a program, but it's a program specifically designed to run another program with various different inputs, and check its result. The test verifies that the program actually behaves the way it's supposed to.

What's interesting about the TDD workflow is that you write the test first, before the code it's testing (it's sometimes called test-first development). So why do this?

By writing the test first, you are forced to think clearly about how the program should behave, and to write those requirements very precisely, as executable code.

You also have to design the API of your program, since you're calling it from the test. So, despite the name, TDD isn't really about the tests. Instead, it's a thinking tool; a process for designing well-structured programs with good APIs. By being your own first user, it's easy for you to see when the code isn't convenient or friendly to use, and fix it.

Let's see how to apply that process now as we start work on our calculator program.

Creating a new project

Every Go project needs two things: a folder on disk to keep its source code in, and a go.mod file which identifies the module, or project name.

If you don't have one already, I recommend you create a new folder on your computer to keep your Go projects in (this can be in any location you like). Then, within this folder, create a subfolder named calculator.

Next, start a shell session using your terminal program (for example, the macOS Terminal app) or your code editor. Set your working directory to the project folder using the cd command (for example, cd ~/go/calculator).

Now run the following command in the shell to create a new Go module in this folder:

go mod init calculator

go: creating new go.mod: module calculator

Creating Go files

So that you don't have to start entirely from scratch, a helpful colleague at Texio Instronics has sent you some Go code which implements part of the calculator's functionality, plus a test for it. In this section you'll add that code to your project folder.

First, using your code editor, create a file in the calculator folder named calculator.go. Copy and paste the following code into it:

// Package calculator provides a library for
// simple calculations in Go.
package calculator

// Add takes two numbers and returns the
// result of adding them together.
func Add(a, b float64) float64 {
    return a + b
}

(Listing 1a)

Don't worry about understanding all the code here for the moment; we'll cover this in detail later. For now, just paste this code into the calculator.go file and save it.

Next, create another file named calculator_test.go containing the following:

package calculator_test

import (
    "calculator"
    "testing"
)

func TestAdd(t *testing.T) {
    t.Parallel()
    var want float64 = 4
    got := calculator.Add(2, 2)
    if want != got {
        t.Errorf("want %f, got %f", want, got)
    }
}

(Listing 1b)

Running the tests

Still in the calculator folder, run the command:

go test

If everything works as it should, you will see this output:

PASS
ok      calculator      0.234s

A failing test

Now that your development environment is all set up, your colleague needs your help. She has been working on getting the calculator to subtract numbers, but there's a problem: the test is not passing. Can you help?

Your colleague has sent you the following test code; copy and paste it into the calculator_test.go file (add it at the end of the file, after the TestAdd function):

func TestSubtract(t *testing.T) {
    t.Parallel()
    var want float64 = 2
    got := calculator.Subtract(4, 2)
    if want != got {
        t.Errorf("want %f, got %f", want, got)
    }
}

(Listing 2a)

Here's the (faulty) code to implement the Subtract function; copy this into the calculator.go file, after the Add function:

// Subtract takes two numbers a and b, and
// returns the result of subtracting b from a.
func Subtract(a, b float64) float64 {
    return b - a
}

(Listing 2b)

Save this file and run the tests:

go test

--- FAIL: TestSubtract (0.00s)
    calculator_test.go:22: want 2.000000, got -2.000000
FAIL
exit status 1
FAIL    calculator      0.178s

This test failure is telling you exactly where the problem occurred:

calculator_test.go:22

It also tells you what the problem was:

want 2.000000, got -2.000000

So what exactly is TestSubtract doing? Let's take a closer look at the body of the function in Listing 2a.

Firstly, the t.Parallel() statement is a standard prelude to tests: it tells Go to run this test concurrently with other tests, which saves time.

The following statement sets up a variable named want, to express what it wants to receive from calling the function under test (Subtract):

var want float64 = 2

Then it calls the Subtract function with the values 4, 2, and stores the result into another variable got:

got := calculator.Subtract(4, 2)

The idea is, having obtained this pair of variables, to compare want with got and see if they are different. If they are, the Subtract function is not working as expected, so the test fails:

if want != got {
    t.Errorf("want %f, got %f", want, got)
}

The function under test

So let's look at the code for the Subtract function (in calculator.go). Here it is:

func Subtract(a, b float64) float64 {
    return b - a
}

GOAL: Get TestSubtract passing!

If you spot a problem in the Subtract function, try altering the code to fix it. Run go test again to check that you got it right. If you're having trouble, try changing the numbers that Subtract is called with to different values, and see if you can figure out what it's doing wrong.

When the test passes, you can move on! If you get stuck, have a look at my solution in Listing 2c.

Writing a function test-first

Excellent work. You now have a calculator that can add and (correctly) subtract. That's a great start. Let's turn to multiplication now.

Up to now you've been running existing tests and modifying existing code. For the first time you're going to write the test, and the function it's testing!

GOAL: Write a test for a function Multiply that, just like the Add and Subtract functions, takes two numbers as parameters, and returns a single number representing the result.

Where should you start? Well, this is a test, so start in the calculator_test.go file. Test functions in Go have to have a name that starts with Test (or Go won't call them when you run go test). So TestMultiply would be a good name, wouldn't it? Let's add the new test to the end of the file, after TestSubtract.

You'll see that TestAdd and TestSubtract look very similar, except for the specific inputs and the expected return value. So start by copying one of those functions, renaming it TestMultiply, and making the appropriate changes.

You'll only need to change the name of the test, the function being called (Multiply instead of Add, for example), perhaps the inputs to it, and the expected value of want.

Something like this will be just fine:

func TestMultiply(t *testing.T) {
    t.Parallel()
    var want float64 = 9
    got := calculator.Multiply(3, 3)
    if want != got {
        t.Errorf("want %f, got %f", want, got)
    }
}

(Listing 3)

When you're done, running the tests should produce a compilation error:

undefined: calculator.Multiply

This makes sense; you haven't written that function yet!

Getting to red

Now we're ready to take the next step, to get to a failing test. That will require us to fix the compile error, which in turn will require writing some code in the calculator package. But perhaps not as much code as you might think!

GOAL: Write the minimum code necessary to get the test to compile and fail.

What is the minimum code necessary to compile? Well, you need to define a Multiply function in the calculator package. It doesn't need to actually do anything yet, so the quickest way to get this program compiling is to have the function return zero:

func Multiply(a, b float64) float64 {
    return 0
}

It's important to verify that the test is correct before you do anything else. Since we know 0 is the wrong answer, the test should fail, shouldn't it? If not, there's some problem with the test.

--- FAIL: TestMultiply (0.00s)
    calculator_test.go:31: want 9.000000, got 0.000000

Getting to green

Perfect! Now you're ready to go ahead and implement Multiply for real. You'll know when you've got it right, because your failing test will start passing. And at that point, you can stop!

If you get stuck, see one possible solution in Listing 3b.

What's next?

Great work! You've just built a Go package test-first. You might like to extend the program further along the same lines, perhaps adding a Divide function, or even more advanced functions. Or if there's a project you've been wanting to work on, but weren't sure how to get started, maybe this has given you a few ideas.

If you enjoyed this tutorial, you can read a longer and more detailed version in my book, For the Love of Go.

Read more

Don't fear the pointer

Don't fear the pointer

CUE is an exciting configuration language

CUE is an exciting configuration language

0