Skip to main content

Testing in Golang

·1747 words·9 mins·
Table of Contents
Lessons learned from `Learn Go with Tests` - This article is part of a series.
Part 3: This Article

Subtests
#

Golang encourages using subtests.

package main

import "testing"

func Hello(name string) string {
  if name == "" {
    return "Hello, World"
  }
  return "Hello, " + name
}

func TestHello(t *testing.T) {
  t.Run("saying hello to people", func(t *testing.T) {
    got := Hello("Chris")
    want := "Hello, Chris"
    assertCorrectMessage(t, got, want)
  })
  t.Run(
    "say 'Hello, World' when an empty string is supplied",
    func(t *testing.T) {
      got := Hello("")
      want := "Hello, World"
      assertCorrectMessage(t, got, want)
    },
  )
}

func assertCorrectMessage(t testing.TB, got, want string) {
  t.Helper()
  if got != want {
    t.Errorf("got %q want %q", got, want)
  }
}
=== RUN   Test_hello
=== RUN   Test_hello/saying_hello_to_people
=== RUN   Test_hello/say_'Hello,_World'_when_an_empty_string_is_supplied
--- PASS: Test_hello (0.00s)
    --- PASS: Test_hello/saying_hello_to_people (0.00s)
    --- PASS: Test_hello/say_'Hello,_World'_when_an_empty_string_is_supplied (0.00s)
PASS

Testing interfaces
#

Golang has a bunch of testing interfaces one can implement depending on what one wants to accomplish. Here are a few examples:

General tests
#

package main

import "testing"

func Hello() string {
	return "Hello, world"
}

func TestHello(t *testing.T) {
	got := Hello()
	want := "Hello, world"

	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}
=== RUN   TestHello
--- PASS: TestHello (0.00s)
PASS

Fuzzing
#

Fuzzing is supported out of the box.

package main

import (
  "testing"
  "unicode/utf8"
)

func Reverse(s string) string {
  b := []byte(s)
  for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
    b[i], b[j] = b[j], b[i]
  }
  return string(b)
}

func FuzzReverse(f *testing.F) {
  testcases := []string{"Hello, world", " ", "!12345"}
  for _, tc := range testcases {
    f.Add(tc) // Use f.Add to provide a seed corpus
  }
  f.Fuzz(func(t *testing.T, orig string) {
    rev := Reverse(orig)
    doubleRev := Reverse(rev)
    if orig != doubleRev {
      t.Errorf("Before: %q, after: %q", orig, doubleRev)
    }
    if utf8.ValidString(orig) && !utf8.ValidString(rev) {
      t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
    }
  })
}
=== RUN   FuzzReverse
=== RUN   FuzzReverse/seed#0
=== RUN   FuzzReverse/seed#1
=== RUN   FuzzReverse/seed#2
--- PASS: FuzzReverse (0.00s)
    --- PASS: FuzzReverse/seed#0 (0.00s)
    --- PASS: FuzzReverse/seed#1 (0.00s)
    --- PASS: FuzzReverse/seed#2 (0.00s)
PASS

Note that fuzz tests must include the Fuzzz prefix.

Benchmarking
#

Benchmarks are recognized by the Benchmark prefix in tests

package main

import "testing"

func Repeat(character string) (repeated string) {
	for i := 0; i < 5; i++ {
		repeated += character
	}
	return
}

func BenchmarkRepeat(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Repeat("a")
	}
}

The framework determines a “good” value for b.N and runs the benchmark accordingly. To run benchmarks do shell go test -bench=.. Then you should get something like the following output:

goos: linux
goarch: amd64
pkg: hello/iteration
cpu: AMD Ryzen 7 5800X 8-Core Processor
BenchmarkRepeat-16      15913407               113.3 ns/op
PASS
ok      hello/benchmark 1.882s

t.Helper
#

t.Helper() is needed when writing helper methods. By doing this, when it fails, the line number reported will be in the function call rather than inside the test helper.

package main

import "testing"

func Hello(name string) string {
  return "Hello, " + name
}

func TestHello(t *testing.T) {
  got := Hello("Chris")
  want := "Hello, Chris"
  assertCorrectMessage(t, got, want)
}

func assertCorrectMessage(t testing.TB, got, want string) {
  t.Helper()
  t.Errorf("got %q want %q", got, want)
}
=== RUN   TestHello
prog_test.go:12: got "Hello, Chris" want "Hello, Chris"
--- FAIL: TestHello (0.00s)
FAIL

Testable examples
#

Testable examples are snippets of Go code that are displayed as package documentation and that are verified by running them as tests. They can also be run by a user visiting the godoc web page for the package and clicking the associated “Run” button.

package main

import "fmt"

func Add(x, y int) int {
	return x + y
}

func ExampleAdd() {
	sum := Add(1, 5)
	fmt.Println(sum)
	// Output: 6
}
=== RUN   ExampleAdd
--- PASS: ExampleAdd (0.00s)
PASS

If you omit // Output: 6 the test example will not run anymore, however it will still be compiled.

Race detector
#

Golang comes with a handy race detector to make your life easier when running concurrent code.

go test -race

Mocking
#

httptest
#

Golang includes the net/http/httptest package to mock http servers in the standard library, this is very helpful when writing tests.

package main

import (
  "net/http"
  "net/http/httptest"
  "testing"
)

func GetStatusCode(url string) (string, error) {
  resp, err := http.Get(url)
  if err != nil {
    return "", err
  }

  return resp.Status, nil
}

func TestGetStatusCode(t *testing.T) {
  mockServer := httptest.NewServer(
    http.HandlerFunc(
      func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
      },
    ),
  )
  defer mockServer.Close()

  want := "200 OK"
  got, _ := GetStatusCode(mockServer.URL)

  if got != want {
    t.Errorf("got %q, want %q", got, want)
  }
}
=== RUN   TestGetStatusCode
--- PASS: TestGetStatusCode (0.00s)
PASS

fstest
#

Go provides the testing/fstest package to mock filesystems

package main

import (
	"io/fs"
	"testing"
	"testing/fstest"
)

func GetFileContent(fileSystem fs.FS, filename string) (string, error) {
	body, err := fs.ReadFile(fileSystem, filename)
	if err != nil {
		return "", err
	}

	return string(body), nil

}

func TestGetFileContent(t *testing.T) {
	testFs := fstest.MapFS{
		"file.md": {Data: []byte("body")},
	}

	got, err := GetFileContent(testFs, "file.md")

	if err != nil {
		t.Fatal(err)
	}

	want := "body"
	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}
}
=== RUN   TestGetFileContent
--- PASS: TestGetFileContent (0.00s)
PASS

A comprehensive write-up on fstest has been written by Ben Congdon.

Property tests
#

Go allows for property tests via the built-in testing/quick library. This allows you to test your code against random inputs

package main

import (
	"strings"
	"testing"
	"testing/quick"
)

type RomanNumeral struct {
	Value  uint16
	Symbol string
}

var allRomanNumerals = []RomanNumeral{
	{1000, "M"},
	{900, "CM"},
	{500, "D"},
	{400, "CD"},
	{100, "C"},
	{90, "XC"},
	{50, "L"},
	{40, "XL"},
	{10, "X"},
	{9, "IX"},
	{5, "V"},
	{4, "IV"},
	{1, "I"},
}

func ConvertToRoman(arabic uint16) string {

	var result strings.Builder

	for _, numeral := range allRomanNumerals {
		for arabic >= numeral.Value {
			result.WriteString(numeral.Symbol)
			arabic -= numeral.Value
		}
	}

	return result.String()
}

func ConvertToArabic(roman string) uint16 {
	var arabic uint16 = 0

	for _, numeral := range allRomanNumerals {
		for strings.HasPrefix(roman, numeral.Symbol) {
			arabic += numeral.Value
			roman = strings.TrimPrefix(roman, numeral.Symbol)
		}
	}

	return arabic
}

func TestPropertiesOfConversion(t *testing.T) {
	assertion := func(arabic uint16) bool {
		t.Log("testing", arabic)
		roman := ConvertToRoman(arabic)
		fromRoman := ConvertToArabic(roman)
		return fromRoman == arabic
	}

	if err := quick.Check(assertion, nil); err != nil {
		t.Error("failed checks", err)
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hello, playground")
}

=== RUN   TestPropertiesOfConversion
    prog_test.go:59: testing 8204
    prog_test.go:59: testing 61907
    prog_test.go:59: testing 51089
    prog_test.go:59: testing 2705
    prog_test.go:59: testing 62112
    prog_test.go:59: testing 31345
    prog_test.go:59: testing 25291
    prog_test.go:59: testing 12293
    prog_test.go:59: testing 25891
    prog_test.go:59: testing 42147
    prog_test.go:59: testing 39197
    prog_test.go:59: testing 51142
    prog_test.go:59: testing 24486
    prog_test.go:59: testing 32988
    prog_test.go:59: testing 2923
    prog_test.go:59: testing 15449
    prog_test.go:59: testing 14345
    prog_test.go:59: testing 11215
    prog_test.go:59: testing 9569
    prog_test.go:59: testing 59569
    prog_test.go:59: testing 48040
    prog_test.go:59: testing 59436
    prog_test.go:59: testing 26845
    prog_test.go:59: testing 34798
    prog_test.go:59: testing 48536
    prog_test.go:59: testing 43567
    prog_test.go:59: testing 37603
    prog_test.go:59: testing 33086
    prog_test.go:59: testing 13605
    prog_test.go:59: testing 7777
    prog_test.go:59: testing 33550
    prog_test.go:59: testing 5594
    prog_test.go:59: testing 41645
    prog_test.go:59: testing 14687
    prog_test.go:59: testing 33943
    prog_test.go:59: testing 40170
    prog_test.go:59: testing 65273
    prog_test.go:59: testing 42607
    prog_test.go:59: testing 53963
    prog_test.go:59: testing 11525
    prog_test.go:59: testing 58320
    prog_test.go:59: testing 41298
    prog_test.go:59: testing 21953
    prog_test.go:59: testing 1199
    prog_test.go:59: testing 41238
    prog_test.go:59: testing 58843
    prog_test.go:59: testing 7721
    prog_test.go:59: testing 27822
    prog_test.go:59: testing 6873
    prog_test.go:59: testing 11785
    prog_test.go:59: testing 31488
    prog_test.go:59: testing 9017
    prog_test.go:59: testing 51631
    prog_test.go:59: testing 31214
    prog_test.go:59: testing 57380
    prog_test.go:59: testing 60907
    prog_test.go:59: testing 26398
    prog_test.go:59: testing 6907
    prog_test.go:59: testing 39703
    prog_test.go:59: testing 1989
    prog_test.go:59: testing 42865
    prog_test.go:59: testing 32234
    prog_test.go:59: testing 6222
    prog_test.go:59: testing 53520
    prog_test.go:59: testing 40739
    prog_test.go:59: testing 1818
    prog_test.go:59: testing 37117
    prog_test.go:59: testing 14014
    prog_test.go:59: testing 54324
    prog_test.go:59: testing 36223
    prog_test.go:59: testing 13444
    prog_test.go:59: testing 51355
    prog_test.go:59: testing 18286
    prog_test.go:59: testing 288
    prog_test.go:59: testing 54756
    prog_test.go:59: testing 22306
    prog_test.go:59: testing 17684
    prog_test.go:59: testing 56489
    prog_test.go:59: testing 25530
    prog_test.go:59: testing 37525
    prog_test.go:59: testing 39480
    prog_test.go:59: testing 57702
    prog_test.go:59: testing 57988
    prog_test.go:59: testing 28377
    prog_test.go:59: testing 53868
    prog_test.go:59: testing 28457
    prog_test.go:59: testing 38090
    prog_test.go:59: testing 6190
    prog_test.go:59: testing 24643
    prog_test.go:59: testing 50892
    prog_test.go:59: testing 16835
    prog_test.go:59: testing 527
    prog_test.go:59: testing 64266
    prog_test.go:59: testing 40598
    prog_test.go:59: testing 60101
    prog_test.go:59: testing 5102
    prog_test.go:59: testing 14169
    prog_test.go:59: testing 53192
    prog_test.go:59: testing 23969
    prog_test.go:59: testing 12735
--- PASS: TestPropertiesOfConversion (0.00s)
PASS

short flag
#

Go provides a short flag to skip tests that take a long time (i.e., acceptance tests) if the -short argument is passed when testing. When running go test -short ./... the following test is skipped

package main

import "testing"

func Add(x, y int) int {
	return x + y
}

func TestAdd(t *testing.T) {
	// This test will be skipped if -short is passed
	if testing.Short() {
		t.Skip()
	}

	want := 5
	got := Add(3, 2)

	if got != want {
		t.Errorf("got %v want %v", got, want)

	}
}

context
#

When you need a context but don’t want to mock out the entire request lifecycle, context.Background() can be used to provide a context for test functions.

t.Parallel
#

t.Parallel() can be used when you want tests in a single package to run in parallel. First the sequential tests without t.Parallel() are run, then the parallel tests.

package main

import "testing"

func returnSomething() string {
	return "something"
}

func Test_returnSomething(t *testing.T) {
	t.Run("test a is run in parallel", func(t *testing.T) {
		t.Parallel()
		got := returnSomething()
		want := "something"
		testHelper(t, got, want)
	})
	t.Run("test b is not run in parallel", func(t *testing.T) {
		got := returnSomething()
		want := "something"
		testHelper(t, got, want)
	})
	t.Run("test c is run in parallel", func(t *testing.T) {
		t.Parallel()
		got := returnSomething()
		want := "something"
		testHelper(t, got, want)
	})
}

func testHelper(t *testing.T, got string, want string)  {
	t.Helper()

	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}
}
=== RUN   Test_returnSomething
=== RUN   Test_returnSomething/test_a_is_run_in_parallel
=== PAUSE Test_returnSomething/test_a_is_run_in_parallel
=== RUN   Test_returnSomething/test_b_is_not_run_in_parallel
=== RUN   Test_returnSomething/test_c_is_run_in_parallel
=== PAUSE Test_returnSomething/test_c_is_run_in_parallel
=== CONT  Test_returnSomething/test_a_is_run_in_parallel
=== CONT  Test_returnSomething/test_c_is_run_in_parallel
--- PASS: Test_returnSomething (0.00s)
    --- PASS: Test_returnSomething/test_b_is_not_run_in_parallel (0.00s)
    --- PASS: Test_returnSomething/test_a_is_run_in_parallel (0.00s)
    --- PASS: Test_returnSomething/test_c_is_run_in_parallel (0.00s)
PASS
Lessons learned from `Learn Go with Tests` - This article is part of a series.
Part 3: This Article