-
Notifications
You must be signed in to change notification settings - Fork 52
Testing
This section introduces testing in Go, which, unsurprisingly, is very much like writing any other kind of Go code.
In this section you’ll learn how to…
- use the
testing
package - use table-driven tests for multiple scenarios under the same test
- do setup/teardown for each test
Go's standard library comes equipped with all you need to test your programs, mainly through the testing package. Let's start with the simplest example of this section.
package main
import (
"testing"
"unicode"
)
func TestRuneIsDigit(t *testing.T) {
c := '4'
if unicode.IsDigit(c) != true {
t.Error("expected rune to be a digit")
}
}
ok github.com/jboursiquot/go-in-3-weeks/testing 0.009s coverage: 0.0% of statements
Success: Tests passed.
The output you see above is from running go test
inside of the folder where the test is located. The following is an example directory listing containing go files and matching test files:
.
├── somefile.go
├── somefile_test.go
├── main.go
In the listing above, somefile.go
contains some form of logic while by convention, somefile_test.go
contains the code that tests the functionality in somefile.go. Regardless of how you name the file, as long as it ends with _test.go
, the Go toolchain (go test
) will treat it as a test file and include it in test runs.
For this exercise, we'll reach back to the library we created during the Getting Started section and write tests for the stringutils
package. Here's the set of functions again:
package stringutils
import "strings"
// Upper returns the uppercase of the given string argument.
func Upper(s string) string {
return strings.ToUpper(s)
}
// Lower returns the lowercase of the given string argument.
func Lower(s string) string {
return strings.ToLower(s)
}
- Create an appropriately-named file for this package (Hint: it needs to end with _test.go).
- Write a test for each function in the package.
- Use the
go
toolchain on the command line to run the tests and see the results.
When you have multiple scenarios you'd like to verify within the same test ideally, you can use a technique called "table-driven tests."
Consider the following:
package main
import (
"fmt"
"reflect"
"testing"
)
func greeting(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
func TestGreeting(t *testing.T) {
tests := []struct {
input string
want string
}{
{input: "Johnny", want: "Hello, Johnny!"},
{input: "世界", want: "Hello, 世界!"},
}
for _, tc := range tests {
got := greeting(tc.input)
if !reflect.DeepEqual(tc.want, got) {
t.Fatalf("expected: %v, got: %v", tc.want, got)
}
}
}
We've set up multiple scenarios to evaluate and subsequently iterate over them to validate the results of calling our greet
function.
A useful and common variation on the above is the use of "sub tests" to capture individual test failures in the own functions which prevents the whole test suite from stopping when we reach a t.Fatalf(...)
statement.
package main
import (
"fmt"
"reflect"
"testing"
)
func greeting(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
func TestGreeting(t *testing.T) {
tests := map[string]struct {
input string
want string
}{
"one": {input: "Johnny", want: "Hello, Johnny!"},
"two": {input: "世界", want: "Hello, 世界!"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := greeting(tc.input)
if !reflect.DeepEqual(tc.want, got) {
t.Fatalf("expected: %v, got: %v", tc.want, got)
}
})
}
}
Note the use of the map[string]struct
instead of the []struct
we had before. The nice side effect here is that this type allows us to use the keys of the map as scenario names for our output.
Modify your solution to Exercise 1 to test at least a couple of different scenarios in each of your tests.
- Use a "table" of scenarios to test multiple cases within each test.
- Understand and make use of sub-tests.
You may be used to setups and teardowns before and after each test from other languages and frameworks. Go has support for this concept through its TestMain function from the testing package.
Here's how it looks:
func TestMain(m *testing.M) {
// setup
code := m.Run()
// teardown os.Exit(code)
}
The idea is to perform any setup you need before calling on m.Run()
and performing any teardowns after it. In the snippet above, we capture and pass an exit code to os.Exit
.
We terminate the test run (and programs in general) with a zero (0) to indicate success or a non-zero for failure. Go here if you'd like to know more about exit code/status.
Modify your solution to Exercise 2 to add a TestMain function. In it, you can setup your test "table" for use within the other test functions and avoid repeating yourself.
- Read the Overview section of the documentation page on the testing package.
- Check out Testing
Go's testing
package has built-in support for benchmarking the performance of Go code. In this section, we'll see how to write some simple benchmarks. That said, it's important to understand that performance tuning in Go (of which benchmarking plays a part) requires more coverage and time than we've dedicated to it here. See Profiling Go Programs as a starting point. Now on to our benchmarks.
Take the common recursive fibonacci implementation below:
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
To benchmark this function, our code, living in something like fib_test.go
, will look something like so:
func benchFib(i int, b *testing.B) {
for n := 0; n < b.N; n++ {
fib(i)
}
}
func BenchmarkFib1(b *testing.B) { benchFib(1, b)}
func BenchmarkFib10(b *testing.B) { benchFib(10, b)}
func BenchmarkFib20(b *testing.B) { benchFib(20, b)}
To run our benchmarks, we invoke go test
with the -bench
flag like so:
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/jboursiquot/go-in-3-weeks/testing/benchmarks
BenchmarkFib1-12 742379979 1.62 ns/op
BenchmarkFib10-12 3690924 299 ns/op
BenchmarkFib20-12 30712 37152 ns/op
PASS
ok github.com/jboursiquot/go-in-3-weeks/testing/benchmarks 4.615sh
Some observations:
- Unlike regular tests that start with Test, benchmarks start with Benchmark.
- The benchmark functions run the target code b.N times during which b.N is adjusted until the benchmark function lasts long enough to be timed reliably.
Write benchmark tests for your stringutils
package's Upper
and Lower
functions.
In this section you learned how to use the testing package to test your Go code. You picked up some efficiency tricks with table-driven tests and learned how to do setup/teardown for each test.