[Awesome Go] Use testify to write better test code in Golang (Go)

testify

Go code (golang) set of packages that provide many tools for testifying that your code will behave as you intend.

Features include:

  • Easy assertions

  • Mocking

  • Testing suite interfaces and functions

Installaction

Standard go get:

1
$ go get -u github.com/stretchr/testify

Usage

assert package

The assert package provides some helpful methods that allow you to write better test code in Go.

  • Prints friendly, easy to read failure descriptions

  • Allows for very readable code

  • Optionally annotate each assertion with a message

See it in action:

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
package yours

import (
"testing"
"github.com/stretchr/testify/assert"
)

func TestSomething(t *testing.T) {

// assert equality
assert.Equal(t, 123, 123, "they should be equal")

// assert inequality
assert.NotEqual(t, 123, 456, "they should not be equal")

// assert for nil (good for errors)
assert.Nil(t, object)

// assert for not nil (good when you expect something)
if assert.NotNil(t, object) {

// now we know that object isn't nil, we are safe to make
// further assertions without causing any errors
assert.Equal(t, "Something", object.Value)

}

}

Every assert func takes the testing.T object as the first argument. This is how it writes the errors out through the normal go test capabilities.

Every assert func returns a bool indicating whether the assertion was successful or not, this is useful for if you want to go on making further assertions under certain conditions.

if you assert many times, use the below:

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
package yours

import (
"testing"
"github.com/stretchr/testify/assert"
)

func TestSomething(t *testing.T) {
assert := assert.New(t)

// assert equality
assert.Equal(123, 123, "they should be equal")

// assert inequality
assert.NotEqual(123, 456, "they should not be equal")

// assert for nil (good for errors)
assert.Nil(object)

// assert for not nil (good when you expect something)
if assert.NotNil(object) {

// now we know that object isn't nil, we are safe to make
// further assertions without causing any errors
assert.Equal("Something", object.Value)
}
}

mock package

The mock package allows the creation of mock objects. These objects can be used to attach expectations to method calls.

An example test function that tests a piece of code that relies on an external object testObj, can setup expectations (testify) and assert that they indeed happened:

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
package main

import (
"testing"
"errors"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)

type MockDatabase struct {
mock.Mock
}

func (db *MockDatabase) connect() error {
// The args object holds the values that we should return for that call, the only thing left to do is return them. We use args.Error(0) to retrieve the first return argument of type error. If we didn’t know the type we could use args.Get(0). If we had multiple return arguments, we can retrieve each of them based on the index. For example: return args.Get(0), args.Error(1), for a method that returns a struct and an error.
args := db.Called()
return args.Error(0)
}

func (db *MockDatabase) sendMessage(message *string) error {
args := db.Called(message)
return args.Error(0)
}

func TestSuccess(t *testing.T) {
db := new(MockDatabase)
message := "Hello"

// Set expectations
db.On("connect").Return(nil)
db.On("sendMessage", &message).Return(nil)

err := Talk(db, &message)

assert.Equal(t, nil, err, "No error")
db.AssertExpectations(t)
}

func TestErrorOnConnect(t *testing.T) {
db := new(MockDatabase)

// Set expectations
db.On("connect").Return(errors.New("Some error"))

message := "Hello"
err := Talk(db, &message)

assert.NotEqual(t, nil, err, "An error is thrown if connection fails")
db.AssertExpectations(t)
}

func TestErrorOnMessage(t *testing.T) {
db := new(MockDatabase)
message := "Hello"

// Set expectations
db.On("connect").Return(nil)
db.On("sendMessage", &message).Return(errors.New("Some error"))

err := Talk(db, &message)

assert.NotEqual(t, nil, err, "An error is thrown if sendMessage fails")
db.AssertExpectations(t)
}

In the tests above, I also used db.AssertExpectations(t). This will fail if any of the expectations (set with db.On...) is not called. This is not necessary for all tests, and you might decide not to use it if your testing style is more loose.

suite package

The suite package provides functionality that you might be used to from more common object oriented languages. With it, you can build a testing suite as a struct, build setup/teardown methods and testing methods on your struct, and run them with go test as per normal.

An example suite is shown below:

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

// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type ExampleTestSuite struct {
suite.Suite
VariableThatShouldStartAtFive int
}

// Make sure that VariableThatShouldStartAtFive is set to five
// before each test
func (suite *ExampleTestSuite) SetupTest() {
suite.VariableThatShouldStartAtFive = 5
}

// All methods that begin with "Test" are run as tests within a
// suite.
func (suite *ExampleTestSuite) TestExample() {
assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}

For a more complete example, using all of the functionality provided by the suite package, look at our example testing suite

For more information on writing suites, check out the API documentation for the suite package.

Suite object has assertion methods:

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
// Basic imports
import (
"testing"
"github.com/stretchr/testify/suite"
)

// Define the suite, and absorb the built-in basic suite
// functionality from testify - including assertion methods.
type ExampleTestSuite struct {
suite.Suite
VariableThatShouldStartAtFive int
}

// Make sure that VariableThatShouldStartAtFive is set to five
// before each test
func (suite *ExampleTestSuite) SetupTest() {
suite.VariableThatShouldStartAtFive = 5
}

// All methods that begin with "Test" are run as tests within a
// suite.
func (suite *ExampleTestSuite) TestExample() {
suite.Equal(suite.VariableThatShouldStartAtFive, 5)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}

References

[1] GitHub - stretchr/testify: A toolkit with common assertions and mocks that plays nicely with the standard library - https://github.com/stretchr/testify

[2] testify · pkg.go.dev - https://pkg.go.dev/github.com/stretchr/testify

[3] Using testify for Golang tests – ncona.com – Learning about computers - https://ncona.com/2020/02/using-testify-for-golang-tests/

[4] GitHub - vektra/mockery: A mock code autogenerator for Golang - https://github.com/vektra/mockery