Introducing the Built-in Go Testing Support

In this lesson, we'll discuss the testing facilities that come with Go.

It turns out that Go, as a pragmatic language, provides rich testing support that includes ways to define and run tests, benchmark the run time and memory usage of your code, provide usage examples, and even test coverage. The topics we will cover include the testing package, the go test command, and writing, and running tests.

The testing package

Go testing starts (and often ends) with the testing package. This package defines types and functions that facilitate writing automated tests for Go packages. The go test command fully supports the testing package and is designed to run your tests and capture the results. Later, we will cover later some other test frameworks, but they are all based on top of the testing package. Here are some of the built-in capabilities you get right away:

  • Self-discovery of tests and benchmarks
  • Nested tests and table-driven tests
  • Skipping tests
  • Verified example code
  • Package-level setup and teardown

Alright, let’s jump right in and see how to go about writing tests.

Writing tests

If you’re familiar with xUnit-style tests, then get ready for something fresh. Go tests use special types defined in the testing package and must implement a special signature:

func TestFoo(t *testing.T) {
}

The testing.T type that is passed to your test has various methods that you call to let the testing infrastructure know if the test failed, including format and log error messages, skip tests, run sub-tests, and more.

For benchmarks there is a different signature:

func BenchmarkFoo(*testing.B) {
}

The role of the testing.B type is similar to the testing.T. It is passed into every benchmark, and it has a slew of functionality that is useful for benchmarking such as reporting memory allocations, reporting metrics, working with timers and running benchmarks in parallel.

The T and B types share a common interface and very imaginatively called TB:

type TB interface {
    Error(args ...interface{})
    Errorf(format string, args ...interface{})
    Fail()
    FailNow()
    Failed() bool
    Fatal(args ...interface{})
    Fatalf(format string, args ...interface{})
    Log(args ...interface{})
    Logf(format string, args ...interface{})
    Name() string
    Skip(args ...interface{})
    SkipNow()
    Skipf(format string, args ...interface{})
    Skipped() bool
    Helper()
}

Since multi-git just controls git, we will not focus on performance, and we will not write any benchmarks. That’s enough theory for now. Let’s write some tests.

Writing simple unit tests

First, unit tests go in the same package that they test in a file with _test.go. This lets the Go test command easily discover all the tests that need to be run.

Here is a very simple Go file that contains a single CalcArea() that calculates the area of a rectangle. It accepts two integers for width and height and returns their product, which is a fantastic way to calculate the area of a rectangle.

If the width or the height are zero or negative it returns an error because any self-respecting rectangle must have width and height that are positive.

package main

import (
    "errors"
)

var errorMessage = "width and height must be positive"

// Calculate the area of a rectangle
func CalcArea(w, h int) (int, error) {
    if w < 1 || h < 1 {
      return 0, errors.New(errorMessage)
    }
    return w * h, nil
}

To test this function, we need at least two test cases: one for the successful branch when the width and height are positive and the second for when at least one of width and height is not positive. Here is the code for the success case. Note that it is in the same main package as the CalcArea() function. It imports the testing package and defines a test function that matches the signature that the testing function accepts. Then, it checks if CalcArea(3, 5) returns 15 without an error. If that’s not the case, it will use the t.Error() or t.Errorf() methods of the testing.T type to report the test as failed. If none of these errors occur, the test will pass.

package main

import (
    "testing"
)

func TestCalcAreaSuccess(t *testing.T) {
    result, err := CalcArea(3, 5)
    if err != nil {
      t.Error("Expected CalcArea(3, 5) to succeed")
    } else if result != 15 {
      t.Errorf("CalcArea(3, 5) returned %d. Expected 15", result)
    }
}

Here is the test code for the failure case. When calling CalcArea(-3, 6) it should fail and return an error. Also, the test checks that the error message is correct.

func TestCalcAreaFail(t *testing.T) {
    _, err := CalcArea(-3, 6)
    if err == nil {
      t.Error("Expected CalcArea(-3, 6) to return an error")
    }
        
    if err.Error() != errorMessage {
      t.Error("Expected error to be: " + errorMessage)      
    } 
}

Now that you know how to write tests, let’s try to run them and see it live.

Get hands-on with 1200+ tech skills courses.