While Go comes with go test and a testing package by default, the experience can be better with testify package. Git hub page introduces it as Go code (golang) set of packages that provide many tools for testifying that your code will behave as you intend.

By the end I found the resulting article as too long (~25 minutes) to read, so I split it into

  1. Part1 introduces assert, talks briefly abot Python and C and shows basics of testify
  2. Part2 introduces table driven testing, more helpers like ElementsMatch or JSONeq
  3. Part3 gets more advanced with test suited and mocks

Test suites

While testing documentation encourages table driven tests and subtests, there are many developers outside, who are used to write Smalltalk like unit tests and this style encourages writing test suites.

Package testify has a solution in form of testify/suite. It comes with setup/teardown methods for the individual test cases or whole site. Additionally there are functions to be run before after the test as well.

Below is an example of a suite with a function running before each test case.

import "github.com/stretchr/testify/suite"

type absSuite struct {
	suite.Suite
	cases []testCase
}

func (s *absSuite) SetupTest() {
    s.cases = []testCase{
        {name: "abs-42", expected: 42, input: -42},
        {name: "abs-1", expected: 1, input: -1},
    }
}

Here we run through initalized test cases. And the test messes up with a shared resource.

func (s *absSuite) TestAbs() {
    for _, tt := range s.cases {
        s.Assert().Equal(tt.expected, Abs(tt.input), "FAIL %s", tt.name)
    }
    s.cases = []testCase{}
    s.Require().Equal(0, len(s.cases))
}

It is not a problem at ALL! The SetupTest creates new array each time from scratch. This is an advantage against table driven tests, where each run can broke shared state, unless the test is not written carefully.

func (s *absSuite) TestNoOfCases() {
    s.Require().Equal(2, len(s.cases))
}

Up to know the tests from shiny new suite does not run. As typical for a good Go code, there is a little magic behind the scenes. So integration of testify.Suite with a testing is done via new testing function.

func TestSuite(t *testing.T) {
    suite.Run(t, new(absSuite))
}

Elementary, my dear Watson.

Mocks

Code, especially in Go, usually communicates with the rest of the world. There are database systems, other microservices, operating systems and so. Sometimes it is easy to simply start the dependency as a part of the unit test and test against real database.

In other cases the cost of the setup is too complex. It is hard to test error states, like canceled context, forbidden login or overloaded system. Then the solution is to mock (imitate) the third party system as a part of our unit testing. It generally brings a few advantages.

  1. helps decoupling of interfaces and implementation
  2. makes failure injection trivial
  3. enhances code coverage

Disadvantages are

  1. it is way more work
  2. devs must pay an attention to keep mock as close simulation to the real world as possible

In general mocks are one of the best tools to test protocols. So here is abs computation to microservices world.

type MathService interface {
    Abs(context.Context, int) (int, error)
}
type MathClient struct{
    svc MathService
}
func (c *MathClient) Abs(ctx context.Context, i int) (int, error) {
    return c.svc.Abs(ctx, i)
}

Code pretends that one need X machine big cluster each time one want to get result. So it is impractical to setup, initialize and run it each time one type go test.

Fortunatelly there is testify/mock to the rescue.

import "github.com/stretchr/testify/mock"
type MathMock struct {
    mock.Mock
}
func (m *MathMock) Abs(ctx context.Context, i int) (int, error) {
    args := m.Called(ctx, i)
    return args.Int(0), args.Error(1)
}

And that’s it. Now MathMock implements necessary interface, so it can be used in the test. So there is a need to write test case

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

    ctx := context.Background()
    cli := &MathClient{svc: &MathMock{}}

    i, err := cli.Abs(ctx, -1)
    require.NoError(err)
    assert.Equal(1, i)
}

And it fail!

assert: mock: I don't know what to return because the method call was unexpected.
        Either do Mock.On("Abs").Return(...) first, or remove the Abs() call.
        This method was unexpected:
                Abs(*context.emptyCtx,int)
                0: (*context.emptyCtx)(0xc000018188)
                1: -1
        at: [main_test.go:164 main.go:46 main_test.go:175

This is because developer must instruct mock object to expect mocked function to be called with a specific arguments. Here comes the On method, which allows to define cases with an arbitrary deep level of a granularity.

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

    ctx := context.Background()
    mathMock := &MathMock{}
    cli := &MathClient{svc: mathMock}

    mathMock.On("Abs", mock.Anything, mock.Anything).
    Return(1, nil)
    i, err := cli.Abs(ctx, -1)
    require.NoError(err)
    assert.Equal(1, i)
}

The problem is it ignores the input parameters at all. Wach call of mocked Abs will return 1, nil. Better version, which simulates the buggy version of Abs is

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

    ctx := context.Background()
    mathMock := &MathMock{}
    cli := &MathClient{svc: mathMock}

    mathMock.On("Abs", mock.Anything, -1).
    Return(1, nil)
    mathMock.On("Abs", mock.Anything, mock.Anything).
    Return(-42, nil)
    i, err := cli.Abs(ctx, -1)
    require.NoError(err)
    assert.Equal(1, i)
    i, err = cli.Abs(ctx, -42)
    require.NoError(err)
    assert.Equal(42, i)
}

Called once

Abs is quite straightforward function to test. One easily assert things like number of calls of a mock

// main.go
// MathClient has a bug and calls abs twice
func (c *MathClient) Abs(ctx context.Context, i int) (int, error) {
    c.svc.Abs(ctx, i)
	return c.svc.Abs(ctx, i)
}
// main_test.go
// test that function is called ONLY once
    mathMock.On("Abs", mock.Anything, -1).
    Return(1, nil).Once()

The same are Twice and Times methods, which allows to assert number of calls inside tested functions. Similar method is Maybe, which don’t fail when mocked method is not called at all.

Timeouts

Timeouts is inevitable part of network programming. Testify allows developers to specify function duration to test the context cancelation. It this example the API is wrong, because context cancelation should immediatelly return err. However it would require adding gorutines and context into MathClient.Abs method.

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

	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 250 * time.Millisecond)
    defer cancel()

	mathMock := &MathMock{}
	cli := &MathClient{svc: mathMock}

	mathMock.On("Abs", mock.Anything, -1).
		Return(1, nil).After(1 * time.Second)
	i, err := cli.Abs(ctx, -1)
	require.NoError(err)
    require.NoError(ctx.Err())
	assert.Equal(1, i)
}

Advanced filtering

It can be impractical to list all the possible cases. Or there can be parts of input, which are constructed in some private functions deep in the chain and they are better to be ignored.

There is MatchedBy function providing argument matching via functions.

// main.go
type AbsRequest struct {
    ctx     context.Context
    i       int
    garbage string
}

//main_test.go
    assert := assert.New(t)
    absMatcher := func(r *AbsRequest) bool {
        return assert.EqualValues(r.i, -1)
    }

    mathMock.
        On("Abs", mock.Anything, mock.MatchedBy(absMatcher)).
        Return(1, nil)

With absMatcher it is

  • easy to ignore unecesarry bits of input structures.
  • use a function from assert package supporting comparsion and diffing of arbitrary nested and complicated structures
  • if matcher function is a closure, then it can access all variables from the outer scope

All parts

  1. Part1 introduces assert, talks briefly abot Python and C and shows basics of testify
  2. Part2 introduces table driven testing, more helpers like ElementsMatch or JSONeq
  3. Part3 gets more advanced with test suited and mocks

Logo by CarbonArc@Flickr: https://www.flickr.com/photos/41002268@N03/23958148837