Getting started with Go and test-driven development

Add comment Ahmed W. Oct 22, 2014

Being a Gopher: Introduction

I was first introduced to Go about a year ago, and I instantly fell in love with the language. It's one of the simplest, easy-to-read, logical, yet extremely powerful languages I've ever learned.

The syntax is straight forward and makes sense even if you have no programming background.

Also, one of the best features about the language is how easy it is to run tests and benchmarks. In this tutorial, I'll explain how to get Go up and running, and then I'll focus on how to write tests and benchmarks.

Installing Go

If you already have Go and $GOPATH setup, you can skip this section.

Binary Installation

Installation is straight forward on most operating systems, and to be able to use/import other people's code you'll need few extra packages, namely Git, Mercurial (hg) and Bazaar (bzr). You can also install Subversion (svn) for the sake of completeness, but it's rare to find someone using it.

  • Arch Linux: sudo pacman -S go git bzr mercurial
  • Debian-based / Ubuntu: sudo apt-get golang git bzr mercurial
  • RedHat-based / CentOS: yum install golang git bzr hg
  • Windows: Download the appropriate Windows installers for each tool, go, git, hg and bzr

Note: that for this tutorial we will only need Go, however, in real-world scenarios you'll need the other tools to import other people's packages.

Manual Installation (*nix-based)

If for some reason your operating system doesn't have a Go package or a recent version, it's fairly straight forward to manually compile and install it.

Note: you need to have a C compiler installed, either gcc or clang.

Note: for this tutorial, I'll assume Go is installed in /opt/go.

  • Download the latest stable .tar.src.gz.
  • cd /opt && tar -xzf /path/to/go1.3.3.src.tar.gz
  • export GOROOT=/opt/go && cd $GOROOT/src
  • Build Go with gcc: ./all.bash
  • Build Go with clang: env CC=clang ./all.bash

all.bash will compile Go and run self-tests to make sure everything is working. You should end up with:

---
Installed Go for linux/amd64 in /opt/go
Installed commands in /opt/go/bin
*** You need to add /opt/go/bin to your PATH.

Configuring The Environment:

Open ~/.bashrc (or the equivalent file for your shell) and add:

export PATH="$PATH:/opt/go/bin" #this is only needed for manual installation.
# $GOPATH "must" be defined otherwise most go tools won't work.
export GOPATH="$HOME/code/go" 

Logout and log back in, or simply source ~/.bashrc and you're ready to Go! (Pun intended.)

If all goes well, running go version should print go version go1.3.3 linux/amd64.

Hello World in Go

Open your favorite editor and type:

package main

import "fmt"

func main() {
    fmt.Println("Hello dot Go")
}

Then save it as hello.go and execute go run hello.go.

Tada!

Things to keep in Mind

Public / Private Modifiers

Like most languages, Go has public / private access modifiers. A public (or "exported" in Go terms) function / variable has to start with a capital (spelling) letter.

So if you're trying to decode json, xml or using reflection and not getting what you're expecting, make sure all the fields in the struct are exported.

Unit testing

Is a way to prevent hair loss for most developers, because no matter how good you are, sometimes you will type the wrong number or even use < instead of >.

Assuming I have function A that calculates something, it gets called in B, which in turn gets called in C.

If I change something in A that returns the wrong number, sometimes it won't be noticeable for a few days since it's not a breaking change.

A way to prevent that is to define tests for each function to make sure they return what they are supposed to return and prevent early baldness.

TDD, or Test-Driven Development

TDD is a very simple concept. You define a test for a function that you didn't write yet, test for the expected behavior, and then write the minimum code to achieve that behavior.

Go Testing, Go.

Go's built-in testing and benchmarking are extremely powerful. We'll explore the different approaches to it.

I've always found the best way to learn is by doing, so let's get started.

A simple example

I have a super cool Fibonacci number generator in $GOPATH/fib/fib.go:

package fib

func FibFunc() func() uint64 {
    var a, b uint64 = 1, 1 // yes, it's wrong
    return func() uint64 {
        a, b = b, a+b
        return a
    }
}

But since my math skills are about as good as a rock, I have to make sure it works right.

To write a unit test all I have to do is create a new file called fib_test.go:

package fib

import "testing"

// use values that you know are right
var tests = []uint64{1, 1, 2, 3, 5, 8, 13, 21, 34, 55}

func TestFibFunc(t *testing.T) {
    fn := FibFunc()
    for i, v := range tests {
        if val := fn(); val != v {
            t.Fatalf("at index %d, expected %d, got %d.", i, v, val)
        }
    }
}

Now to test the awesome library:

$ cd $GOPATH/fib/fib.go
$ go test
--- FAIL: TestFibFunc (0.00s)
        fib_test.go:11: at index 1, expected 1, got 2.
FAIL
exit status 1
FAIL    fib       0.001s

Oops, I just have to fix FibFunc() changing var a, b uint64 = 1, 1 to var a, b uint64 = 0, 1 then rerun the test:

$  go test
PASS
ok      fib       0.001s

And that's how easy it is to write a unit test in Go, all files named *_test.go are ignored by go build, and will only run by go test.

For a test function to be run it has to have this signature:

func TestTestName(t *testing.T) { //Test 
    //
}

Selecting which tests to run

In a real world scenario, you could have a lot of tests, and only one of them breaks.

Now, these tests might take sometime to run, and having to rerun them while trying to fix the bug can be annoying and slow.

Luckily go test provides a way to run the selected tests based on a regexp.

Using go test -run=regexp:

$ go test -v -run 'FibFunc'
=== RUN TestFibFunc
--- PASS: TestFibFunc (0.00s)
PASS
ok      fib       0.001s

Tada!

Table Tests

A common practice for testing a function is to test it with multiple inputs and see the expected results, using a table of input and expected output.

For this example we will use a slice of anonymous structs:

// here we define the input and expected output
var mul_tests = []struct {
    a, b     int
    expected int
}{
    {1, 1, 1},
    {2, 2, 4},
    {3, 3, 9},
    {4, 4, 16},
    {5, 5, 25},
}

func TestMul(t *testing.T) {
    for _, mt := range mul_tests {
        if v := Mul(mt.a, mt.b); v != mt.expected {
            t.Errorf("Mul(%d, %d) returned %d, expected %d", mt.a, mt.b, v, mt.expected)
        }
    }
}

Benchmarking

The go test tool handles benchmarks as well, however they don't run by default.

Defining a benchmark works the same way as testing:

func BenchmarkFibFunc(b *testing.B) {
    fn := FibFunc()
    for i := 0; i < b.N; i++ {
        _ = fn()
    }
}

Notice the i < b.N part, N changes internally by the testing package, so you have to use the for loop that way if you want an accurate benchmark.

Running the benchmark

$ go test -bench=.
PASS
BenchmarkFibFunc        1000000000               2.51 ns/op
ok      fib       2.764s

Where -bench=., just like -run, accepts a regexp that matches the benchmarks you want to run.


And that's it! Best of luck coding and testing in Go!

0 comments


Or enter your name and Email
No comments have been posted yet.