Testing Go Code With Testify

While Go supports testing out of the box, there are multiple testing packages that make writing unit tests a bit less verbose. The most popular testing packages are Testify, Gocheck and GoMock. On the moment of writing (March 2016) Testify is developing more actively and has significantly more committers. In this post we will learn how to write unit tests with it.

Testify packages

If you have Java or .Net background, Testify will look like an old friend. It provides assertions very similar to jUnit family together with mocking support. Testify plays nice with default Go testing package and includes three convenient packages: assert, require, and mock.

package assert

Let's start with an example:

package person

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

...

func TestFind(t *testing.T) {  
  service := ...
  firstName, lastName := service.find(someParams)
  assert.Equal(t, "John", firstName)
  assert.Equal(t, "Dow", lastName)
}

In this example we test find method of person service. We created service, called its method find and checked that firstName and lastName matched expected values. We used method Equal here, but assert package provides lots of other methods, similar to jUnit family. Check them out.

Note that assert returns boolean value. It returns true if assertion is successful and false otherwise, allowing other assertions to be executed in spite of failures upstream. This could come as a surprise to developers with jUnit background, because Assert in jUnit throws AssertionError. Next package require provides exactly that behaviour.

package require

require package is very similar to assert, except for it terminates test if assertion is not successful.

Let's rewrite the example with require:

package person

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

...

func TestFind(t *testing.T) {  
  service := ...
  firstName, lastName := service.find(someParams)
  require.Equal(t, "John", firstName)
  require.Equal(t, "Dow", lastName)
}

This test is similar to the one with assert. The only difference would be if require.Equal(t, "John", firstName) does not hold, then test will be terminated and the next line require.Equal(t, "Dow", lastName) will not be executed.

package mock

Mocking can be very helpful if you need to make a method return result based on input parameters passed.

type serviceMock struct {  
  mock.Mock
}

func (s *serviceMock) hello(name string) string {  
  args := s.Called(name)
  return "Hello, " + args.String(0)
}

In this example we return a tailored greeting message based on name passed as a param.

Example

I believe that an example is a the best way to demonstrate an idea. Below is a weird calculator package created to serve as an example of testing with Testify. You can get this example from GitHub.

calculator.go:

package calculator

type Random interface {  
  Random(limit int) int
}

type Calculator interface {  
  Add(x, y int) int
  Subtract(x, y int) int
  Multiply(x, y int) int
  Divide(x, y int) int
  Random() int
}

func newCalculator(rnd Random) Calculator {  
  return calc{
    rnd: rnd,
  }
}

type calc struct {  
  rnd Random
}

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

func (c calc) Subtract(x, y int) int {  
  return x - y
}

func (c calc) Multiply(x, y int) int {  
  return x * y
}

func (c calc) Divide(x, y int) int {  
  return x / y
}

func (c calc) Random() int {  
  return c.rnd.Random(100)
}

calculator_test.go:

package calculator

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

type randomMock struct {  
  mock.Mock
}

func (o randomMock) Random(limit int) int {  
  args := o.Called(limit)
  return args.Int(0)
}

func TestAdd(t *testing.T) {  
  calc := newCalculator(nil)
  assert.Equal(t, 9, calc.Add(5, 4))
}

func TestSubtract(t *testing.T) {  
  calc := newCalculator(nil)
  assert.Equal(t, 1, calc.Subtract(5, 4))
}

func TestMultiply(t *testing.T) {  
  calc := newCalculator(nil)
  assert.Equal(t, 20, calc.Multiply(5, 4))
}

func TestDivide(t *testing.T) {  
  calc := newCalculator(nil)
  assert.Equal(t, 5, calc.Divide(20, 4))
}

func TestRandom(t *testing.T) {  
  rnd := new(randomMock)
  rnd.On("Random", 100).Return(7)
  calc := newCalculator(rnd)
  assert.Equal(t, 7, calc.Random())
}

Bottom line

There is a quite strong opinion in Gopher community that default testing package should be preferred all the time in spite of its simplicity and limitations. However if you get used to jUnit style or would like to make your tests a little bit less verbose, Testify could be a better choice.

comments powered by Disqus