Golang testing — gotest.tools assertions
Published on Aug 16, 2018 by Vincent Demeester.
Introduction
Let’s take a closer look at gotest.tools assertions packages. This is mainly about assert, assert/cmp and
assert/opt.
Package assert provides assertions for comparing expected values to actual values. When assertion fails a helpful error message is printed.
There is two main functions (Assert and Check) and some helpers (like NilError, …). They all take a *testing.T as
a first argument, pretty common across testing Go libraries. Let’s dive into those !
Assert and Check
Both those functions accept a Comparison (we’ll check what it is later on) and fail the test when that comparison
fails. The one difference is that Assert will end the test execution at immediately whereas Check will fail the test
and proceed with the rest of the test case. This is similar to FailNow and Fail from the standard library
testing. Both have their use cases.
We’ll Use Assert for the rest of the section but any example here would work with Check too. When we said
Comparison above, it’s mainly the BoolOrComparison interface — it can either be a boolean expression, or a
cmp.Comparison type. Assert and Check code will be smart enough to detect which one it is.
assert.Assert(t, ok) assert.Assert(t, err != nil) assert.Assert(t, foo.IsBar())
So far not anything extra-ordinary. Let’s first look at some more helper functions in the assert package and quickly
dive a bit deeper with Comparison.
More assert helpers
The additional helper functions are the following
Equaluses the==operator to assert two values are equal.DeepEqualusesgoogle/go-cmpto assert two values are equal (it’s close toreflect.DeepEqualbut not quite). We’ll detail a bit more the options part of this function withcmp.DeepEqual.Errorfails if the error isnilor the error message is not the expected one.ErrorContainsfails if the error isnilor the error message does not contain the expected substring.ErrorTypefails if the error isnilor the error type is not the expected type.NilErrorfails if the error is notnil.
All those helper functions have a equivalent function in the cmp package that returns a Comparison. I, personally,
prefer to use assert.Check or assert.Assert in combination with cmp.Comparison as it allows me to write all my
assertions the same way, with built-ins comparison or with my own — i.e. assert.Assert(t, is.Equal(…), "message" or
assert.Assert(t, stackIsUp(c, time…), "another message").
cmp.Comparison
This is where it get really interesting, gotest.tools tries to make it as easy as possible for you to create
appropriate comparison — making you test readable as much as possible.
Let’s look a bit at the cmp.Comparison type.
type Comparison func() Result
It’s just a function that returns a cmp.Result, so let’s look at cmp.Result definition.
type Result interface { Success() bool }
Result is an interface, thus any struct that provide a function Success that returns a bool can be used as a
comparison result, making it really easy to use in your code. There is also existing type of result to make it even
quicker to write your own comparison.
ResultSuccessis a constant which is returned to indicate success.ResultFailureandResultFailureTemplatereturn a failed Result with a failure message.ResultFromErrorreturnsResultSuccessiferris nil. OtherwiseResultFailureis returned with the error message as the failure message. It works a bit like theerrors.Wrapfunction of thegithub.com/pkgs/errorspackage.
The cmp package comes with a few defined comparison that, we think, should cover a high number of use-cases. Let’s
look at them.
Equality with Equal and DeepEqual
Equal uses the == operator to assert two values are equal and fails the test if they are not equal.
If the comparison fails Equal will use the variable names for x and y as part of the failure message to identify the actual and expected values.
If either x or y are a multi-line string the failure message will include a unified diff of the two values. If the values only differ by whitespace the unified diff will be augmented by replacing whitespace characters with visible characters to identify the whitespace difference.
On the other hand…
DeepEqual uses google/go-cmp (http://bit.do/go-cmp) to assert two values are equal and fails the test if they are not equal.
Package https://godoc.org/gotest.tools/assert/opt provides some additional commonly used Options.
Using one or the other is as simple as : if you wrote your if with == then use Equal, otherwise use DeepEqual.
DeepEqual (and usually reflect.DeepEqual) is used when you want to compare anything more complex than primitive
types. One advantage of using cmp.DeepEqual over reflect.DeepEqual (in an if), is that you get a well crafted
message that shows the diff between the expected and the actual structs compared – and you can pass options to it.
assert.Assert(t, cmp.DeepEqual([]string{"a", "b"}, []string{"b", "a"})) // Will print something like // --- result // +++ exp // {[]string}[0]: // -: "a" // +: "b" // {[]string}[1]: // -: "b" // +: "a" foo := &someType(a: "with", b: "value") bar := &someType(a: "with", b: "value") // the following will succeed as foo and bar are _DeepEqual_ assert.Assert(t, cmp.DeepEqual(foo, bar))
When using DeepEqual, you may end up with really weird behavior(s). You may want to ignore some fields, or consider
nil slice or map the same as empty ones ; or more common, your struct contains some unexported fields that you
cannot use when comparing (as they are not exported 😓). In those case, you can use go-cmp options.
Some existing one are :
EquateEmptyreturns a Comparer option that determines all maps and slices with a length of zero to be equal, regardless of whether they are nil.IgnoreFieldsreturns an Option that ignores exported fields of the given names on a single struct type. The struct type is specified by passing in a value of that type.IgnoreUnexportedreturns an Option that only ignores the immediate unexported fields of a struct, including anonymous fields of unexported types.SortSlicesreturns a Transformer option that sorts all[]V- … and more 👼
gotest.tools also defines some and you can define yours ! As an example, gotest.tools defines TimeWithThreshold
and DurationWithThreshold that allows to not fails if the time (or duration) is not exactly the same but in the
specified threshold we specified. Here is the code for DurationWithThreshold for inspiration.
// DurationWithThreshold returns a gocmp.Comparer for comparing time.Duration. The // Comparer returns true if the difference between the two Duration values is // within the threshold and neither value is zero. func DurationWithThreshold(threshold time.Duration) gocmp.Option { return gocmp.Comparer(cmpDuration(threshold)) } func cmpDuration(threshold time.Duration) func(x, y time.Duration) bool { return func(x, y time.Duration) bool { if x == 0 || y == 0 { return false } delta := x - y return delta <= threshold && delta >= -threshold } }
Another good example for those options is when you want to skip some field. In docker/docker we want to be able to
easily check for equality between two service specs, but those might have different CreatedAt and UpdatedAt values
that we usually don’t care about – what we want is to make sure it happens in the past 20 seconds. You can easily define
an option for that.
func cmpServiceOpts() cmp.Option { const threshold = 20 * time.Second // Apply withinThreshold only for the following fields metaTimeFields := func(path cmp.Path)bool { switch path.String() { case "Meta.CreatedAt", "Meta.UpdatedAt": return true } return false } // have a 20s threshold for the time value that will be passed withinThreshold := cmp.Comparer(func(x, y time.Time) bool { delta := x.Sub(y) return delta < threshold && delta > -threshold }) return cmp.FilterPath(metaTimeFields, withinThreshold) }
I recommend you look at the gotest.tools/assert/opt documentation to see which one are defined and how to use them.
Errors with Error, ErrorContains and ErrorType
Checking for errors is very common in Go, having Comparison function for it was a requirement.
Errorfails if the error isnilor the error message is not the expected one.ErrorContainsfails if the error isnilor the error message does not contain the expected substring.ErrorTypefails if the error isnilor the error type is not the expected type.
Let’s first look at the most used : Error and ErrorContains.
var err error // will fail with : expected an error, got nil assert.Check(t, cmp.Error(err, "message in a bottle")) err = errors.Wrap(errors.New("other"), "wrapped") // will fail with : expected error "other", got "wrapped: other" assert.Check(t, cmp.Error(err, "other")) // will succeed assert.Check(t, cmp.ErrorContains(err, "other"))
As you can see ErrorContains is especially useful when working with wrapped errors.
Now let’s look at ErrorType.
var err error // will fail with : error is nil, not StubError assert.Check(t, cmp.ErrorType(err, StubError{})) err := StubError{"foo"} // will succeed assert.Check(t, cmp.ErrorType(err, StubError{})) // Note that it also work with a function returning an error func foo() error {} assert.Check(t, cmp.ErrorType(foo, StubError{}))
Bonus with Panics
Sometimes, a code is supposed to panic, see Effective Go (#Panic) for more information. And thus, you may want to make sure you’re code panics in such cases. It’s always a bit tricky to test a code that panic as you have to use a deferred function to recover the panic — but then if the panic doesn’t happen how do you fail the test ?
This is where Panics comes handy.
func foo(shouldPanic bool) { if shouldPanic { panic("booooooooooh") } // don't worry, be happy } // will fail with : did not panic assert.Check(t, cmp.Panics(foo(false))) // will succeed assert.Check(t, cmp.Panics(foo(true)))
Miscellaneous with Contains, Len and Nil
Those last three built-in Comparison are pretty straightforward.
Containssucceeds if item is in collection. Collection may be a string, map, slice, or array.If collection is a string, item must also be a string, and is compared using
strings.Contains(). If collection is a Map, contains will succeed if item is a key in the map. If collection is a slice or array, item is compared to each item in the sequence using=reflect.DeepEqual()=.Lensucceeds if the sequence has the expected length.Nilsucceeds if obj is a nil interface, pointer, or function.
// Contains works on string, map, slice or arrays assert.Check(t, cmp.Contains("foobar", "foo")) assert.Check(t, cmp.Contains([]string{"a", "b", "c"}, "b")) assert.Check(t, cmp.Contains(map[string]int{"a": 1, "b": 2, "c": 4}, "b")) // Len also works on string, map, slice or arrays assert.Check(t, cmp.Len("foobar", 6)) assert.Check(t, cmp.Len([]string{"a", "b", "c"}, 3)) assert.Check(t, cmp.Len(map[string]int{"a": 1, "b": 2, "c": 4}, 3)) // Nil var foo *MyStruc assert.Check(t, cmp.Nil(foo)) assert.Check(t, cmp.Nil(bar()))
But let’s not waste more time and let’s see how to write our own Comparison !
Write your own Comparison
One of the main aspect of gotest.tools/assert is to make it easy for developer to write as less boilerplate code as
possible while writing tests. Writing your own Comparison allows you to write a well named function that will be easy
to read and that can be re-used across your tests.
Let’s look back at the cmp.Comparison and cmp.Result types.
type Comparison func() Result type Result interface { Success() bool }
A Comparison for assert.Check or assert.Check is a function that return a Result, it’s pretty straightforward to
implement, especially with cmp.ResultSuccess and cmp.ResultFailure(…) (as seen previously).
func regexPattern(value string, pattern string) cmp.Comparison { return func() cmp.Result { re := regexp.MustCompile(pattern) if re.MatchString(value) { return cmp.ResultSuccess } return cmp.ResultFailure( fmt.Sprintf("%q did not match pattern %q", value, pattern)) } } // To use it assert.Check(t, regexPattern("12345.34", `\d+.\d\d`))
As you can see, it’s pretty easy to implement, and you can do quite a lot in there easily. If a function call returns an
error inside of your Comparison function, you can use cmp.ResultFromError for example. Having something like
assert.Check(t, isMyServerUp(":8080")) is way more readable than a 30-line of code to check it.
Conclusion…
… and that’s a wrap. We only looked at the assert package of gotest.tools so far, but it’s already quite a bit to process.
We’ve seen :
- the main functions provided by this package :
assert.Assertandassert.Check - some helper functions like
assert.NilError, … - the
assert/cmp, andassert/optsub-package that allows you to write more customComparison
Next time, we’ll look at the skip package, that is a really simple wrapper on top of testing.Skip function.
**