Shermans's Notes
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Testing in Golang

Table Driven Tests

Table-driven tests format your tests to ensure that you are as DRY as possible while making it easy to add more test cases. There are plenty of resources about this, which I will link below; however, here is my appreciation.

 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
package testing  
  
import (  
   "github.com/stretchr/testify/assert"  
   "strings"   
   "testing"
)  
  
func TestFoo(t *testing.T){  
   tests := map[string]struct{  
      in string  
      expect []string  
   }{  
      "splits a list at ':'": {  
         in: "foo:bar:baz",  
         expect: []string{"foo", "bar", "baz"},  
      },  
      "does not split a list at '-'": {  
         in: "foo-bar:baz",  
         expect: []string{"foo-bar", "baz"},  
      },  
   }  
  
   for name, data := range tests {  
      t.Run(name, func(t *testing.T) {  
         result := strings.Split(data.in, ":")  
         assert.Equal(t, data.expect, result)  
      })   
   }

The key here is that one function body is doing the tests and that it is easy enough to add a new case by adding the correct data.

If you have more complex data, you can easily add more to the test struct to model this:

 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
package testing  
  
import (  
   "github.com/stretchr/testify/assert"  
   "strings"   
   "testing"
)  

func TestFoo(t *testing.T) {  
  
   type Input struct {  
      foo string  
      bar int  
      baz bool  
   }  
  
   type Expect struct {  
      count  int  
      errMsg string  
   }  
  
   tests := map[string]struct {  
      in     Input  
      expect Expect  
   }{  
      "adds len of string to int when baz is true": {  
         in:     Input{foo: "Hello!", bar: 5, baz: true},  
         expect: Expect{count: 11},  
      },  
      "len of string only when baz is false": {  
         in:     Input{foo: "Hello!", bar: 5, baz: false},  
         expect: Expect{count: 6},  
      },  
      "has an error when the string is empty": {  
         in:     Input{foo: "", bar: 5, baz: true},  
         expect: Expect{count: 0, errMsg: "cannot be empty"},  
      },  
   }  
  
   for name, data := range tests {  
      t.Run(name, func(t *testing.T) {  
  
         var msg string  
         var count int  
  
         // Check for error first  
         if len(data.in.foo) == 0 {  
            msg = "message cannot be empty!"  
         } else {  
            // Do the test otherwise  
            if data.in.baz {  
               count = len(data.in.foo) + data.in.bar  
            } else {  
               count = len(data.in.foo)  
            }  
         }  
  
         // Check expected outputs  
         if data.expect.errMsg != "" {  
            assert.Contains(t, msg, data.expect.errMsg)  
            assert.Equal(t, 0, count)  
            return  
         }  
         assert.Equal(t, data.expect.count, count)  
      })  
   }  
}

It’s a contrived example, but you can see how you can use more complex data in your tests.

  • Notice that you can rely on default values for non pointer values, when writing your tests; this will reduce the repetition of tests. also including a value shows intent
  • You can also construct default values for the local structs, if required, by defining a struct before your tests and adding merging the values from the test data into it before starting the work and assertions.

See more:

Asserting with Testify

Go ships with bare bones testing framework, which is shocking if you have come from more feature-complete testing platforms. Luckily the community provides!

1
go get "github.com/stretchr/testify/assert"`

Testing Suites

Go’s test runner does not have a built-in concept of suites. Separating unit and integration tests can become an issue.

There is a way to do this with creative use of the build tags. This does come with the caveat that you must change your test commands to select the suite you want.

From go 1.17, the tag is now //go:build. Versions before used // +build; both syntaxes are currently valid but prefer the newer, former version
  • Add //go:build unit at the top of the unit test files.
  • Add //go:build integration at the top of integration test files.
  • For helpers used in both, you can define //go:build unit | integration to ensure the file is compiled for both
  • To run these tests, you must include the -tags option in the command:
    • go test -tags unit ./...
    • go test -tags integration ./...
    • go test -tags unit,integration ./...

Testing Env Vars

Setting environment variables with os.SetEnv() and retrieving with os.GetEnv() in testing can lead to race conditions. I have found this behaviour in a project where a test ended up asserting a value set in the env in the previous test.

A quick Google found this GitHub Issue which states : >Unfortunately, people often don’t realize that setting environment variables in tests using os.Setenv will set those variables for the entire lifetime of the process.

Also without test harness support for env variables, it also becomes a problem in parallel tests.

In order to get around this, make sure to write more testable functions that take a collated version of env vars.

For example:

1
2
3
func run(env map[string][string]) {
	// Do something here
}

A function written like the above, can be tested with ease bypassing a map[string][string], without having to worry about the race conditions of access to the env.

Update

The testing.T does actually contain a function t.SetEnv which will set an env var and clean it up after the test for you, using os.Setenv, however, it still cannot be used in parallel tests. I’d still prefer to use a defined map. It has the added bonus of keeping

Testing flag

See using-and-testing-flag. The TL;DR is:

  • Don’t set up flags in init if you want to write tests
  • Don’t use the default flag set if you want to write tests
  • Do use a custom flag set and ensure you can pass args to the tests