diff --git a/src/blog/posts/2024/5/learning-go-day-5.md b/src/blog/posts/2024/5/learning-go-day-5.md new file mode 100644 index 0000000..29028b8 --- /dev/null +++ b/src/blog/posts/2024/5/learning-go-day-5.md @@ -0,0 +1,127 @@ +--- +title: "Learning Go: Day Five" +date: 2024-05-05T08:00:00.0Z +tags: + - learning + - go +excerpt: "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 work[^1], 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](https://blog.jetbrains.com/go/2022/11/22/comprehensive-guide-to-testing-in-go/) 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`: + +```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: + +```bash +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: + +```bash +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: + +```bash +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: + +```go +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: + +```bash +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: + +```go +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 .`: + +```bash +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 project[^2]. One of the more useful things in the Jetbrains post includes the [Testify package](https://github.com/stretchr/testify), 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 \ No newline at end of file