77 views
# Go Testing > This week we will go though the basics of testing in [Go](https://golang.org) > > 📘 [go mills(): 1th Mar 2023 Go Testing](...) Table of Contents: <!-- toc --> ## Overview - Writing tests is the same as writing programs - No 3rd-party tools needed, `go test` is it! - Each test is a function of the form `TestXXX` - Benchmark tests in `BenchmarkXXX` - Fuzzing tests in `FuzzXXX` - Examples in `ExampleXXX` - Setup / Tear-down in `TestMain(...)` - Uses the [testing][testing] package As you can see, the built-in support for testing in Go is quite powerful with a lot of features and yet quite simple to use! (_unlike other languages, I'm looking at you Python! 🐍_) We won't be able to go through all of this today! Let me know if you want another session on some specific testing facilities in Go! 👌 ## The Basics Let's say we have a module / package: ```go // Package fizz is an implementation of the FizzBuzz classic programming question where the idea is // to return different strings for a range of integers from 0 through to N printing Fizz for integers // divisible by 3, Buzz for integers divisible by 5, FizzBuzz for integers divisible by both e and 5 // and the input integer otherwise, and so on.... package fizz import "fmt" // Buzz returns Fizz for integers divisible by 3, Buzz for integers divisible by 5 and FizzBuzz // for integers divisible by both 3 and 5, otherwise returns the integer. func Buzz(n int) string { if n%15 == 0 { return "FizzBuzz" } if n%3 == 0 { return "Fizz" } if n%5 == 0 { return "Buzz" } return fmt.Sprintf("%d", n) } ``` We can write a unit test for this package by simply writing a `TestBuzz` function like so: ```go package fizz_test import ( "testing" "fizz" ) func TestBuzz(t *testing.T) { s := fizz.Buzz(0) if s != "FizzBuzz" { t.Errorf("expected FizzBuzz got %s", s) } } ``` **Note** that we named the package in this case `fizz_test` and not `fizz` as you'd expect. Why? This forces the Go compiler to treat the `_test.go` modules differently with a different module path so that you are **forced** to properly import your library as if you were a consumer of that library. ## Running Tests Running tests is easy. Just run `go test` or `go test .`, which will run all the test modules found in the current package, or if you have tests in sub-packages (_which you should!_) run `go test ./...`. For example: ```console $ go test . ok fizz 0.151s ``` You can see each test run by passing the `-v` flag (verbose): ```console $ go test -v . === RUN TestBuzz --- PASS: TestBuzz (0.00s) PASS ok fizz 0.105s ``` ### Getting Coverage Computing test coverage is as easy as adding the `-cover` flag to `go test`, like so: ```console $ go test -cover . ok fizz 0.358s coverage: 28.6% of statements ``` ## Table Driven Tests Now that was a pretty boring unit test 🤣 let's get more serious! We only managed to write one test case and covered about ~30% of the library's logic 😢 Table Driven Tests to the secure! 🥳 ```go package fizz_test import ( "testing" "fizz" ) func TestBuzz(t *testing.T) { cases := []struct { Name string Input int ExpectedOutput string }{ {"divisible by 3", 3, "Fizz"}, {"divisible by 5", 5, "Buzz"}, {"divisible by both 3 and 5", 15, "FizzBuzz"}, {"not divisible by 3 or 5", 7, "7"}, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { actualOutput := fizz.Buzz(tc.Input) if actualOutput != tc.ExpectedOutput { t.Errorf("expected %s got %s", tc.ExpectedOutput, actualOutput) } }) } } ``` Running this test with `go test -v -cover .` we can see we now have a much greater confidence in our library's function: ```console $ go test -v -cover . === RUN TestBuzz === RUN TestBuzz/divisible_by_3 === RUN TestBuzz/divisible_by_5 === RUN TestBuzz/divisible_by_both_3_and_5 === RUN TestBuzz/not_divisible_by_3_or_5 --- PASS: TestBuzz (0.00s) --- PASS: TestBuzz/divisible_by_3 (0.00s) --- PASS: TestBuzz/divisible_by_5 (0.00s) --- PASS: TestBuzz/divisible_by_both_3_and_5 (0.00s) --- PASS: TestBuzz/not_divisible_by_3_or_5 (0.00s) PASS coverage: 100.0% of statements ok fizz 0.292s coverage: 100.0% of statements ``` Now don't be fooled! Getting 100% test cover is not the aim of unit testing. It just to happens that this simple contrived example is pretty easy to get 100% test cover. You **should not** be aiming for this, use the 80/20 rule, and use your best judgment. My rule of thumb is: > Aim to cover the expected public facing interface of your library or program. That is test the behavior of your library or program. ## Debugging Tests What happens when a test fails? If we add another test case for example: ```go {"bogus", -3", ""} ``` And run the test suite we get: ```console $ go test -v . === RUN TestBuzz === RUN TestBuzz/bogus fizz_test.go:26: expected got Fizz === RUN TestBuzz/divisible_by_3 === RUN TestBuzz/divisible_by_5 === RUN TestBuzz/divisible_by_both_3_and_5 === RUN TestBuzz/not_divisible_by_3_or_5 --- FAIL: TestBuzz (0.00s) --- FAIL: TestBuzz/bogus (0.00s) --- PASS: TestBuzz/divisible_by_3 (0.00s) --- PASS: TestBuzz/divisible_by_5 (0.00s) --- PASS: TestBuzz/divisible_by_both_3_and_5 (0.00s) --- PASS: TestBuzz/not_divisible_by_3_or_5 (0.00s) FAIL FAIL fizz 0.386s FAIL ``` So we know that `TestBuzz/bogus` failed and we didn't the expected output, and the assertion failed. ## Better Assertions One thing I like to do is use a _slightly_ better assertion library from [stretchr][stretchr] called [testify][testify] which has a couple of nice sub-packages called [assert][assert] and [require][require]. Let's see how this improves things: ```console $ go test -v . === RUN TestBuzz === RUN TestBuzz/bogus === CONT TestBuzz fizz_test.go:29: Error Trace: /Users/prologic/tmp/foo/fizz_test.go:29 Error: Not equal: expected: "" actual : "Fizz" Diff: --- Expected +++ Actual @@ -1 +1 @@ - +Fizz Test: TestBuzz === RUN TestBuzz/divisible_by_3 === RUN TestBuzz/divisible_by_5 === RUN TestBuzz/divisible_by_both_3_and_5 === RUN TestBuzz/not_divisible_by_3_or_5 --- FAIL: TestBuzz (0.00s) --- PASS: TestBuzz/bogus (0.00s) --- PASS: TestBuzz/divisible_by_3 (0.00s) --- PASS: TestBuzz/divisible_by_5 (0.00s) --- PASS: TestBuzz/divisible_by_both_3_and_5 (0.00s) --- PASS: TestBuzz/not_divisible_by_3_or_5 (0.00s) FAIL FAIL fizz 0.380s FAIL ``` Ah that's much better! 😅 We can see clearly what's going on and what failed! ## Conclusion There is much more depth to cover when writing tests in Go, including benchmark tests, fizzing, providing examples (_which are included in documentation_) and writing setup and tear-down functionality. There are also some techniques and patterns for writing good integration testing that we didn't cover here, and Go 1.20 included the `-cover` flag to `go build` which means you can now build binaries with coverage enabled for coverage guided testing or to measure how much of your program's code _actually_ used. The example code we walked through today can be found in its entirely at [go.mills.io/fizz][go.mills.io/fizz] ---- [testing]: https://pkg.go.dev/testing [stretchr]: https://github.com/stretchr [assert]: https://pkg.go.dev/github.com/stretchr/testify@v1.8.2/assert [require]: https://pkg.go.dev/github.com/stretchr/testify@v1.8.2/require [go.mills.io/fizz]: https://pkg.go.dev/go.mills.io/fizz