4.5 KiB
title | date | tags | excerpt | ||
---|---|---|---|---|---|
Learning Go: Day Five | 2024-05-05T08:00:00.0Z |
|
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.