Get started with Go testing

Learn how to create and run unit tests in Go with the programming language’s built-in test management tools.

Get started with Go testing
Thinkstock

Modern programming languages include native tools for building and running unit tests on code bases. The Go language has its own such toolset, in the form of the testing module and the go test command.

In this article we’ll cover the basics of writing unit tests using Go, and deploying those tests side-by-side along with the code.

A basic Go app with a unit test

Here is a simple (albeit totally unoptimized) program that calculates Fibonacci sequences in Go.

package main

import "fmt"

func FibInt(a int) int {
	if a < 2 {
	  return a
	}
	return FibInt(a - 1) + FibInt(a - 2)
}

func main() {
    fmt.Println("FibInt:",FibInt(30))
}

If we save this program as main.go in a directory named fib, we can run it by executing go run .\fib.

To create a test for this program, we need to create a file in the same directory named main_test.go. The _test suffix in the file name provides a hint to the testing module that this file is the test suite for main.go.

The contents of main_test.go will look like this:

package main
 
import "testing"
 
func TestFibInt(t *testing.T) {
    result := FibInt(30)
    if result != 832040 {
        t.Error("result should be 832040, got", result)
    }
}

Tests in a unit test file consist of a function starting with the word Test (the capital T is important) and some other term beginning with a capital letter. Again, this is a signal to testing: It indicates which functions to use as unit tests. Functions that don’t follow this naming convention are considered internal to the test suite. That is, they’re functions that might be called from within the test functions but aren’t executed directly as part of the test suite run.

Unit tests import every public function in the file they correspond to. In this case, all the public methods in main.go are available (chiefly FibInt), and we run them to conduct our tests.

Each test function takes in a variable of type testing.T, which is used to raise errors when a test fails. testing.T objects can also be used to log messages if needed.

Running Go unit tests

To run unit tests for a Go application, just type go test <app>, where <app> is the name of the directory containing the application. The results print to the console:

$ go test .\fib2\
PASS
ok      fib/fib2        0.084s

The time stamp in the rightmost column is the total time taken to run the tests.

If a test fails, the location of the failing test (file, line number) gets printed to the console, along with any error messages.

$ go test .\fib2\
--- FAIL: TestFibInt (0.00s)
    main_test.go:8: result should be 832040, got 0
FAIL
FAIL    fib/fib2        0.068s
FAIL

The “result should be” message here is courtesy of the t.Error() statement.

Running subtests in Go

Go’s testing module provides a way to run subsets of tests instead of everything at once. One way to do this is simply to invoke tests by name. For instance, the sole function in the above test suite could be invoked with the following command in the appropriate source directory:

go test -run FibInt

This command would run tests only for the FibInt function and nothing else.

Another way you can selectively run tests is by passing arguments into the test runner that can be used by the tests. Let’s replace all of the test functions in our testing file with the following:

func tFibInt(t *testing.T) {
    result := FibInt(30)
    if result != 832040 {
        t.Error("result should be 832040, got", result)
    }
}

func TestAll(t *testing.T){
    t.Run("a=0", tFibInt)
}

The t.Run() command takes two arguments — the first matches against parameters passed to go test, and the second is the name of a test function. Note that we’ve renamed TestFibInt to tFibInt so that it isn’t invoked directly by the test runner.

We can selectively run this set of tests a number of different ways:

  • go test -run All
    Runs every test function with All in the name. In this case, the initial argument of TestAll. t.Run() will be ignored because we passed no parameters to be used to match against it.
  • go test -run All/a=0
    Same as above, except t.Run() will run only when the first parameter has a string that matches a=0. In this case, it will run tFibInt.
  • go test -run All/a=
    As above, except t.Run() will run only when the first parameter has a string that matches a=. In this case, it will run tFibInt, as a partial match also works.
  • go test -run All/a=1
    As above, but this will run nothing, because none of the t.Run() functions has a first parameter that matches a=1.

From these kinds of options, you can run tests in various combinations — for instance, as a programmatic way to test a specific feature or feature set.

Copyright © 2021 IDG Communications, Inc.