Testing a.k.a. automated testing, is the practice of writing small programs that check that the code under test behaves as expected for certain inputs, which are either carefully chosen to exercise certain features or randomised to ensure broad coverage.
Go’s approach relies on one command go test, and set of conventions for writing test functions that go test can run. The lightweight mechanism is effective for pure testing, and it extends naturally to benchmarks and systematic examples for documentation.
The go test Tool
The go test subcommand is a test driver for Go packages that are organized using certain conventions. In a package directory, files whose name ends with _test.go are not part of packet built by go build but are a part of it when built by go test.
Within test files, three types of functions are treated differently:
- tests: A test function whose name begins with Test, exercise program logic for correct behavior.
- benchmarks: A benchmark function whose name begins with Benchmark, measures the performance of some operation.
- examples: An example function whose name begins with Example, provides machine-checked documentation.
The go test scans the _test.go files for these special functions, generates a temporary main package that calls them all in the proper way, builds and runs it, reports the results and then cleans up.
Test Functions
Each test file must import the testing package. Test functions have the following signature:
Func TestName(t *testing.T) {
// …
}
Test function names must begin with Test; the optional suffix Name must being with a capital letter. The t parameter provides methods for reporting test failures and logging additional information.
Example
Following is an example of validating palindrome using a function IsPalindrome that reports if the string reads the same forward and backwards.
golang/src/palindrome/palindrome.go
package palindrome
import "unicode"
// IsPalindrome ... reports whether s reads the same forward and backward
// Unicode implementation
func IsPalindrome(s string) bool {
var letters []rune
for _, r := range s {
if unicode.IsLetter(r) {
letters = append(letters, unicode.ToLower(r))
}
}
for i := range letters {
if letters[i] != letters[len(letters)-i-1] {
return false
}
}
return true
}
In the same directory, file palindrome_test.go contains functions to test the functionality. It shows example usage of test and benchmark functions.
TestPalindromeTabular shows a table-drive methodology, which is very common in Go. It is easy to add new entries as needed. Special care should be taken to print enough information to identify the failed case. In case of result mismatch, t.Errorf is used to display additional information.
golang/src/palindrome/palindrome_test.go
package palindrome
import "testing"
func TestPalindromeTabular(t *testing.T) {
var tests = []struct {
input string
want bool
}{
{"", true},
{"a", true},
{"aa", true},
{"ab", false},
{"kayak", true},
{"detartrated", true},
{"palindrome", false},
}
for idx, test := range tests {
got := IsPalindrome(test.input)
if got != test.want {
t.Errorf("Test%d: input:%q got:%v not same as want:%v", idx, test.input, got, test.want)
}
}
}
func TestFrenchPalindrome(t *testing.T) {
if !IsPalindrome("été") {
t.Error(`IsPalindrome("été") = false`)
}
}
func TestCanalPalindrome(t *testing.T) {
input := "A man, a plan, a canal: Panama"
if !IsPalindrome(input) {
t.Errorf(`IsPalindrome(%q) = false`, input)
}
}
func TestCanalNotPalindrome(t *testing.T) {
input := "A man a plan, a canal: anama"
if IsPalindrome(input) {
t.Errorf(`IsPalindrome(%q) = false`, input)
}
}
func BenchmarkIsPalindrome(b *testing.B) {
for i := 0; i < b.N; i++ {
IsPalindrome("A man, a plan, a canal: Panama")
}
}
Simplest way to run these test is to execute
go test
The -v flag prints the name and execution time of each test in the package
go test -v
The -run flag takes regular expression as argument to run only those tests whose name matches the regular expression.
go test -run=”French|Canal”
Coverage
The degree to which a test suite exercise the package under test is called the test’s coverage. Statement coverage is the simplest and most widely used heuristic.
Go cover tool, which is integrated into go test, is used to measure coverage and help identify gaps in the tests. Following command can be used to find the coverage:
go test -cover
Following command is an enhanced version of go test -cover. It renders a HTML page that visualises line-by-line coverage of each affected .go file. The first half go test -coverprofile=cover.out
runs go test -cover
under the hood and saves the output to the file cover.out. The second half go tool cover -html=cover.out
generates a HTML page in a new browser window, based on the contents of cover.out.
go test -coverprofile=cover.out && go tool cover -html=cover.out
Benchmark Functions
Benchmarking is the practice of measuring the performance of a program on a fixed workload. A Benchmark function looks like a test function but with the Benchmark prefix and a testing.B parameter, which provides performance measurement related additional functions.
To run benchmark functions:
go test -bench=.
The -benchmem command-line flag will include memory allocation statistics in the report
go test -bench=. -benchmem
Tips
Command line arguments
One of the interesing problem is to pass comamnd line arguments to the code under test. This can be done easily by setting os.args variable as shown below:
golang/src/args/args.go
package main
import (
"flag"
"fmt"
)
func main() {
passArguments()
}
func passArguments() string {
user := flag.String("user", "root", "Username for the server")
flag.Parse()
fmt.Printf("Username is %q\n", *user)
userString := *user
return userString
}
golang/src/args/args_test.go
package main
import (
"os"
"testing"
)
func TestArgs(t *testing.T) {
expected := "one"
// save and restore global variable os.Args
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"args", "-user=one"}
actual := passArguments()
if actual != expected {
t.Errorf("Test failed, expected: '%s', got: '%s'", expected, actual)
}
}