Skip to content

Testing

Johnny Boursiquot edited this page Oct 24, 2023 · 3 revisions

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

The testing package

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.

Exercise 1: Using the testing package

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)
}

Objectives

  1. Create an appropriately-named file for this package (Hint: it needs to end with _test.go).
  2. Write a test for each function in the package.
  3. Use the go toolchain on the command line to run the tests and see the results.

What you need to know

Table-Driven Tests

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.

Exercise 2: Using table-driven testing

Modify your solution to Exercise 1 to test at least a couple of different scenarios in each of your tests.

Objectives

  1. Use a "table" of scenarios to test multiple cases within each test.
  2. Understand and make use of sub-tests.

Setup/Teardown

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.

Exercise 3: Using setup/teardown

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.

What you need to know

Benchmarks

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:

  1. Unlike regular tests that start with Test, benchmarks start with Benchmark.
  2. 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.

Exercise 4: Writing benchmarks

Write benchmark tests for your stringutils package's Upper and Lower functions.

Summary

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.