Golang std testing

From wikinotes

Go ships with a minimalist test suite.

Documentation

testing https://pkg.go.dev/testing@go1.18.3

Usage

go test                              # run tests in CWD
go test ./...                        # run all tests
go test -run FooTest                 # run top-level tests containing 'FooTest'
go test -run 'FooTest/My TableTest'  # run specific table test
go test -run FooTest ./internal/foo  # run tests matching 'FooTest in package at '/internal/foo'
go test ./internal/foo -v            # run internal/foo tests, and print

you can also create an ad-hoc test-package and run exclusively a ad-hoc test.go package,
but this is not considered the norm

go test internal/foo/bar_test.go

Example

The builtin go test framework is fairly minimalist.
Tests are just functions, but you can define subtests with t.Run(...).
There are no builtin assertions.

Tests are typically kept alongside code.

// myproject/mypackage/mylib.go

package mypackage

func Hello(name string) string {
    return "Hello, " + name
}
// myproject/mypackage/mylib_test.go

package mypackage

import "testing"

func TestHello(t *testing.T) {
    res := Hello("Adam")
    if res != "Hello, Adam" {
        t.Errorf("Hello() result did not match")
    }
}

Sample Strategies

1:1 function:test with subtests

The mangos library uses a really clean approach.
A library of assertions is created, then each test

  • Instantiates a struct
  • Asserts on all public methods of the struct using t.Run(...)
https://github.com/nanomsg/mangos/blob/master/test/certs_test.go test cases (per struct)
https://github.com/nanomsg/mangos/blob/master/test/util.go test assertions

Table Tests

See subtests below.

func TestHello(t *testing.T) {
    tcases := []struct {                         // <-- slice of structs containing testdata
        test       string
        name       string
        expects    string
    }{
        { test: "Valid Name", name: "Adam", },
        { test: "Nil Name", name: nil, },
        { test: "Invalid Name", name: nil, },
    }

    for _, tcase := range tcases {
        t.Run(tcase.test, func(t *testing.T) {   // <-- t.Run() evaluates each case
            if res := hello.Hello(tcase.name); res != tcase.expects {
                t.Errorf("Failed because...")
            }
        }
    }
}

Override Function

Go allows you to assign a function to a global variable.
You abstract a real method call with this substitute,
and override the substitute within a test.

Note that this only works if a function is saved to a var

  • You cannot override regular functions in tests (ex. func Foo() {})
  • You cannot override methods in tests (ex. func (this *Foo) Bar() {})
// foo.go
var removeFile = os.Remove

func DoThing(path string) {
    removeFile(path)
}
// foo_test.go

func TestDoThing(t *testing.T) {
    var removedPath string
    removeFile = func(path string) {
        removedPath = path
    }

    DoThing("/var/abc.txt")
    if removedPath != "/var/abc.txt" {
        t.Errorf("Failed because...")
    }
}

Interface/Substitute Concretion

If you're covering a large set of methods throughout your application,
you probably want to abstract it with an interface.

You can define a package with an exported variable with a concretion of the interface in production,
within tests, you can override the variable with a testable stub object.

Filesystem operations are a good example of this (but consider using golang afero or similar).

production


// internal/filesystem/interface.go
package filesystem

type Interface interface {
    Remove(name string) error
}
// internal/filesystem/raw.go
package filesystem
import "os"

type Raw struct{} // implements 'example.com/x/internal/filesystem.Filesystem'

func (this *Raw) Remove(name string) error {
    return os.Remove(name)
}
// internal/filesystem/fs.go
package filesystem

var Fs = Raw{} // exported variable, that we can override for entire application within tests
// foo.go
package foo
import "example.com/x/internal/filesystem"

func Foo() error {
    return filesystem.Fs.Remove("/var/tmp/foo.txt")
}


testing


// internal/filesystem/stub.go
package filesystem

type Stub struct { // implements 'example.com/x/internal/filesystem.Filesystem'
    RemoveError error
}

func (this *Raw) Remove(name string) error {
    return this.RemoveError
}
// foo_test.go
package foo
import "example.com/x/internal/filesystem"

func TestMain(t *testing.T) {
    // swap out fs with our stub
    var ExpectedError = errors.New("Expected")
    filesystem.Fs = filesystem.Stub{RemoveError: ExpectedError}

    func t.Run("Raises Error", func(t *testing.T) {
        err := foo.Foo()
        assert.Error(t, err, ExpectedError)
    })
}


Components

Assertions

There are no assertions, you are responsible for tests and messages.

func TestHello(t *testing.T) {
    // log message and fail (but continue executing)
    t.Errorf("expected: %v\nreceived: %v", expects, received)
    t.Fail()         // mark test as failed, but continue
    t.FailNow()      // mark test as failed and stop executing

    t.Skip("Reason") // log, and stop executing

    t.TempDir()      // provides a tempdir that is deleted once test finishes running
}

Subtests

Evaluate subtests under one function using t.Run("TESTNAME", ...).

These can be expressed as Table Tests:

import "example.com/x/hello"                     // <-- despite sharing package, must import package in test

func TestHello(t *testing.T) {
    tcases := []struct {                         // <-- slice of structs containing testdata
        test       string
        name       string
        expects    string
    }{ { test: "ValidName", name: "Adam", },
       { test: "NilName", name: nil, } }

    for _, tcase := range tcases {
        t.Run(tcase.test, func(t *testing.T) {   // <-- t.Run() evaluates each case
            res := hello.Hello(tcase.name)
            if res != tcase.expects {
                t.Errorf("Failed because...")
            }
        }
    }
}

Or you can define simply unique subtests

import "github.com/stretchr/testify/assert"
import "regexp"
import "testing"

func TestHello(t *testing.T) {
    t.Run("Begins with H", func(t *testing.T) {
        res := hello.Hello("Alex")
        assert.Regexp(t, regexp.MustCompile("^h"), res)
    }

    t.Run("Last word is name", func(t *testing.T) {
        res := hello.Hello("Alex")
        assert.Regexp(t, regexp.MustCompile("Alex$"), res)
    })
}

You can also implement setup/teardown this way using defer functions.

Setup/Teardown

There is no framework-set way of doing this, just run a function.

func setup(t *testing.T) {
    t.Log("setup")
}

func teardown(t *testing.T) {
    t.Log("teardown")
}

func TestHello(t *testing.T) {
    t.Run("Tests that ..", func(t *testing.T) {
        t.setup(t)
        defer t.teardown(t)
        // ...
    }
}

Benchmarking

There are tools for benchmarking. See docs

Logging

Tests have a logger, that you can use to print information during test runs.
(only shown on failing tests)

func TestThing(t *testing.T) {
    t.Log("Show this while testing")
}

Fuzzing

There are tools for fuzzing tests. See docs