Test-driven development with Go
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
}
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)
}
}
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)
}
}
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
}
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)
}
}
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.