Testify: make Go testing easy 2/3
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
- Part1 introduces
assert
, talks briefly abot Python and C and shows basics oftestify
- Part2 introduces table driven testing, more helpers like
ElementsMatch
orJSONeq
- Part3 gets more advanced with test suited and mocks
Table driven testing
Table driven testing is common and usefull go idiom. And it is good idea to use with sub tests as well.
type testCase struct {
name string
input int
expected int
}
func TestAbs(t *testing.T) {
require := require.New(t)
cases := []testCase{
{
name: "abs-42",
input: -42,
expected: 42,
},
{
name: "abs-1",
input: -1,
expected: 1,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T){
require.Equalf(tt.expected, Abs(tt.input), "fail in test %s", t.Name())
})
}
}
To be honest this is an overkill for a simple function like Abs
. It becomes
handy when dealing with a function with complex input/output data structures
and complex internal states. It is possible to run individual subtests, the
testing
package deals with all of this well.
One problem remains. Code uses shared instance of require
. Subtest failure
stops all other tests from run. This is unecessary and can be easilly fixed by
creating new require
object each time. On the other hand as tests are
supposed to succeed all the time, early failure in a subtest might not be a
problem in all cases.
@@ -29,6 +28,7 @@
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T){
+ require := require.New(t)
require.Equalf(tt.expected, Abs(tt.input), "fail in test %s", t.Name())
})
}
Interesting testify helpers
While Equal
is probably the most used function, there are 56 functions
available for assert
and require
packages. At least for 1.4.0 version.
go doc github.com/stretchr/testify/assert | grep '^func ' | grep -v '.*f(' | wc -l
56
And frankly I doubt there are a lof of developers, which have used them all. Neither myself. So here is a short list of features I found totally handy.
Equal methods
Method Equal
is probably the most commonly used one. It is defined with
interface{}
type, which is a way how to tell Go compiler that developer is
going to check types during runtime. However Equal
method checks the type
equality during test run, so it is not possible to pass two same numbers with a
distinct types.
func TestEqual(t *testing.T) {
assert := assert.New(t)
assert.Equal(42, 42)
// line below will FAIL
assert.Equal(int(42), uint(42))
}
There is helper EqualValues
ignoring types
func TestEqual(t *testing.T) {
assert := assert.New(t)
// and this is OK
assert.EqualValues(int(42), uint(42))
}
Or if it happens there are two variables of different types, however with
identical structure EqualValues
does the work as well.
func TestStructs(t *testing.T) {
assert := assert.New(t)
type foo struct {Foo string}
type bar struct {Foo string}
aFoo := foo{Foo: "spam"}
aBar := bar{Foo: "spam"}
// line below will FAIL
assert.Equal(aFoo, aBar)
assert.EqualValues(aFoo, aBar)
}
Let’s not forget that this is still Go. While something similar can be done by Ruby or maybe Python libraries, in Go developer must be explicit
func TestMagic(t *testing.T) {
assert := assert.New(t)
jsn := `{"foo": "spam"}`
yml := `foo: spam`
// this is string comparsion and obviously fail
assert.EqualValues(jsn, yml)
}
JSON/YAML helpers
However! Noth json and yaml are common formats on Internet. Go code usually
receive and send JSONS over HTTP and reads and write YAML files in order to
build or run container, or to build Helm charts to deploy things to Kubernetes.
It is not surprising to have related helpers in testify
.
func TestJSON(t *testing.T) {
assert := assert.New(t)
jsn := `{"foo": "spam"}`
jsn2 := `{
"foo":
"spam"
}`
assert.JSONEq(jsn, jsn2)
}
Making an example for YAMLeq
would be trivial, so lets reimplement the magic test with a real check
import "github.com/ghodss/yaml"
func y2j(yml string) (string, error){
ret, err := yaml.YAMLToJSON([]byte(yml))
if err != nil {
return "", err
}
return string(ret), nil
}
func TestNoMagic(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
jsn := `{"foo": "spam"}`
yml, err := y2j(`foo: spam`)
require.NoError(err)
assert.JSONEq(jsn, yml)
}
Ignoring the tiny helper converting the yaml to json format, the test is longer
only beause of explicit error check. And error checks are usually done by
require
as there is no reason to continue.
Error/NoError
The most common usage of Error
method is with a require
package. If there is
a fail in operation supposed to not fail, then there is usually no reason to
continue with a test. And vice versa. It is handy to halt the test in a case of
unexpected failure during execution.
resp, err := http.Get(url)
require.NoError(err)
// main.go
func Err() error {
return fmt.Errorf("bind: already in use")
}
// main_test.go
func TestErr(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
err := Err()
require.Error(err)
assert.Equal(
fmt.Errorf("bind: already in use"),
err)
}
The check NoError
is simply the same, only checks the case err == nil
.
// main.go
func NoErr() error {
return nil
}
// main_test.go
func TestNoErr(t *testing.T) {
require := require.New(t)
err := NoErr()
require.NoError(err)
}
ElementsMatch
Sometimes developer do not care about an order of items in JSON array. Most
likelly it depends on some B-tree magic of used SQL engine table index. All
developer care about is that the right values are in place. How many times
there is an unecessary sort in place just to make unit test pass? Not
anymore with a testify
and ElementsMatch
helper.
func TestNoOrder(t *testing.T) {
assert := assert.New(t)
expected := `{"data": [1, 2, 3]}`
actual := `{"data": [2, 1, 3]}`
assert.JSONEq(expected, actual)
}
Of course JSONEq
can’t help. It works for native data only.
func TestNoOrder2(t *testing.T) {
assert := assert.New(t)
expected := []int{1, 2, 3}
actual := []int{2, 1, 3}
assert.ElementsMatch(expected, actual)
}
Panics
In go panic is something one should use carefully. At the same time it can be the only one way to return an error from an interface, which does not make that possible. Consider following totally fabricated example. There is standard interface for math functions as they typically do not return an error. And consider there is a special ultra fast implementation. Which unfortunatelly does work for a subset of inputs. The only way to deal with it and still implement the interface is to panic for invalid inputs.
And NotPanics
and Panics
methods will make sure we can test such functions
well.
type AbsComputer interface {
Abs(i int) int
}
func UberFastAbs(i int) int {
if i == -1 {
return 1
}
panic(i)
}
// main_test.go
func TestUberFastAbs(t *testing.T) {
assert := assert.New(t)
assert.NotPanics(func (){UberFastAbs(-1)})
assert.Panics(func() {UberFastAbs(42)})
}
All parts
- Part1 introduces
assert
, talks briefly abot Python and C and shows basics oftestify
- Part2 introduces table driven testing, more helpers like
ElementsMatch
orJSONeq
- Part3 gets more advanced with test suited and mocks
Logo by CarbonArc@Flickr: https://www.flickr.com/photos/41002268@N03/23958148837