lewisdale.dev/src/blog/posts/2024/5/learning-go-day-5.md
Lewis Dale b29dca1648
All checks were successful
Build and copy to prod / build-and-copy (push) Successful in 2m7s
Revert restructuring the learning go titles
2024-05-01 10:01:13 +01:00

4.5 KiB

title date tags excerpt
Learning Go: Day Five 2024-05-05T08:00:00.0Z
learning
go
For Day Five, I'm going to look at how to write tests

Testing is important! I'm an advocate for Test-Driven Development in my work1, so it's quite important that I work out how to test what I'm writing before I go any further. For today, I'm using this really helpful blog post from Jetbrains as a guide.

Creating a test

First of all, I need a separate file for my tests. The convention is apparently to store tests alongside the code they're testing, so I'll create maths_test.go:

// maths_test.go
package maths

import "testing"

func TestMultiply(t *testing.T) {
    result := Multiply(2, 5)
    if result != 10 {
        t.Errorf("Got %d when expecting %d", result, 10)
    }
}

This wrapping function seems to be used similarly to a describe block if you're writing tests using jest in Javascript. That is, it's a descriptor for a collection of tests, not necessarily a single test.

Tests are ran using the go test command, however when I ran mine I got this output:

go test
?       lewisdale.dev/learn-go  [no test files]

It turns out I had to specify the package because I'm running it from the module root:

go test lewisdale.dev/learn-go/maths
ok      lewisdale.dev/learn-go/maths    0.138s

And then searching tells me I can also use ./... as the package name to run all tests recursively:

go test ./...
ok      lewisdale.dev/learn-go/maths    0.138s

Triangulating tests

These are call table-driven tests, and they amount to iterating over an array (or slice!) of inputs and running a test for each input:

func TestMultiply(t *testing.T) {
    var inputs = []struct {
        a, b, expected int
    }{
        {2, 5, 10},
        {10, 100, 1000},
        {12, 15, 180},
    }

    for _, input := range inputs {
        t.Run(fmt.Sprintf("%d x %d = %d", input.a, input.b, input.expected), func (t *testing.T) {
            if result := Multiply(input.a, input.b); result != input.expected {
                t.Errorf("Got %d when expecting %d", result, input.expected)
            }
        })
    }
}

The command line doesn't give much information by default, but by adding the verbose (-v) flag to the test command we get much better output:

go test ./... -v
?       lewisdale.dev/learn-go  [no test files]
=== RUN   TestMultiply
=== RUN   TestMultiply/2_x_5_=_10
=== RUN   TestMultiply/10_x_100_=_1000
=== RUN   TestMultiply/12_x_15_=_180
--- PASS: TestMultiply (0.00s)
    --- PASS: TestMultiply/2_x_5_=_10 (0.00s)
    --- PASS: TestMultiply/10_x_100_=_1000 (0.00s)
    --- PASS: TestMultiply/12_x_15_=_180 (0.00s)
PASS
ok      lewisdale.dev/learn-go/maths    0.109s

Fuzzing tests

This is a pretty cool feature. Go has a built-in test fuzzer that produces random values and can be used to find edge cases:

func FuzzMultiply(f *testing.F) {
    f.Add(2, 5)
    f.Fuzz(func (t *testing.T, a, b int) {
        Multiply(a, b)
    })
}

And then run using cd maths && go test -fuzz .:

go test -fuzz .
fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed
fuzz: elapsed: 0s, gathering baseline coverage: 1/1 completed, now fuzzing with 10 workers
fuzz: elapsed: 3s, execs: 1358234 (452687/sec), new interesting: 0 (total: 1)
fuzz: elapsed: 6s, execs: 2742335 (461339/sec), new interesting: 0 (total: 1)
fuzz: elapsed: 9s, execs: 4126851 (461439/sec), new interesting: 0 (total: 1)
fuzz: elapsed: 12s, execs: 5525464 (466204/sec), new interesting: 0 (total: 1)
^Cfuzz: elapsed: 12s, execs: 5737472 (471088/sec), new interesting: 0 (total: 1)
PASS
ok      lewisdale.dev/learn-go/maths    12.594s

This ran my function almost 6 million times in 12 seconds, which is wild. This is a really useful tool for picking up those hard-to-find bugs, but because it will run until there's an error it almost certainly won't be something to use in CI.

That's all I'm going to cover on testing today, there's quite a lot more there that I'll start to pick through as I start working on an actual project2. One of the more useful things in the Jetbrains post includes the Testify package, which looks like a testing framework that wraps testing for less-verbose tests.


  1. At least, when I remember to do it, otherwise it's DDT. ↩︎

  2. Which I will decide on in the next couple of days ↩︎