Why I don't use a third-party assertion library in Go unit tests
Tags: go
Published: 2024-05-20 18:54:02

---
title: Why I don't use a third-party assertion library in Go unit tests
published: true
description: 
tags: go
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-05-20 17:56 +0000
---
TL;DR: I don't need it, and you probably don't either. I'll explain below.

As we know of course, Go ships with a built-in unit testing framework in its standard library and toolchain, as explained in the [`testing`](https://pkg.go.dev/testing) package documentation. Assuming you have some code to test:

```go
package add

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

The documentation shows a test like:

```go
package add

import "testing"

func TestAdd(t *testing.T) {
	got := Add(1, 2)

	if got != 4 {
		t.Errorf("Add(1, 2) = %d; want 4", got)
	}
}
```

And if the test fails you get an error like: `Add(1, 2) = 3; want 4`.

Of course, as soon as people saw this, the third-party assertion helper libraries started appearing. The most popular one seems to be [testify](https://github.com/stretchr/testify) (although I've never used it). Personally, I thought that the explicit check would be good enough for me, but it's true that after writing a bunch of tests, the boilerplate does seem unnecessarily verbose.

But do we really need a third-party library to abstract it? Pre-Go generics, I might have said yes. But post-generics, I think it's pretty simple to write a helper function directly:

```go
package assert

import "testing"

func Equal[V comparable](t *testing.T, got, expected V) {
	t.Helper()

	if expected != got {
		t.Errorf(`assert.Equal(
t,
got:
%v
,
expected:
%v
)`, got, expected)
	}
}
```

As you can see, generics are the key here, especially the fact that we specify that types must be _comparable._ Before generics, we would have had to use things like reflection, or even just not use a helper function at all. To my eyes, many pre-generics Go code patterns seem to be about avoiding things that would have required generics to express safely. Eg, we were using a generic comparison operator `got != 4` directly instead of encapsulating the comparison in a function call that would have needed generics.

So, how does this look like in practice? Here's the above test case rewritten to use the helper:

```
func TestAdd(t *testing.T) {
	assert.Equal(t, Add(1, 2), 4)
}
```

Much nicer, I think, even though we lose the ability to format a custom error message. Speaking of error message, let's look at that:

```
$ go test gt/add
--- FAIL: TestAdd (0.00s)
    add_test.go:9: assert.Equal(
        t,
        got:
        3
        ,
        expected:
        4
        )
FAIL
FAIL	gt/add	0.119s
FAIL
```

Sure, we don't have a custom error message that tells us what operation was performed here, _but_ we do have the exact line number in the test so we can just see for ourselves. And also, VSCode and I assume other good editors can show test results inline:

![Inline test result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zegzz10v6jyf8ulj5vc8.png)

With a good editor showing inline results, it's pretty obvious what the code under test did and what result it expected.

## API design considerations

Deciding on the right API and the right output is a little tricky, but it's worth taking the time to do it right. The function signature is important:

```go
func Equal[V comparable](t *testing.T, got, expected V)
```

Notice that we pass in the _actual_ ie 'got' value _first,_ and the _expected_ value second. The reason for this interconnects with my testing philosophy: one test function should _try_ (as hard as possible) to assert _one_ thing. Often, we need to test more complex data. In these cases, I believe the best approach is to snapshot the data as a string and assert on the string. Eg:

```go
assert.Equal(
	t,
	dataframe1.Join(dataframe2).String(),
	`---------
| a | b |
---------
| 1 | 2 |`,
)
```

This allows us to elegantly capture the entire 'behaviour' of the code under test, and update it quickly in the future if needed. If one day the dataframe result changes, the test failure output will show the new value and we can update it with a simple copy-paste. Think of it as a proto-[snapshot testing](https://jestjs.io/docs/snapshot-testing) style. Maybe in the future editor support tools will even be able to offer a one-click way to update the expected value.

Finally, note the layout of the failure message: `assert.Equal(t, got: x, expected: y)`. This is deliberately chosen to teach the user how to call this helper even if they don't start by reading its documentation. By just looking at the error, they learn that the 'actual' value is the _second_ argument, and the _expected_ value is the _third._ This is informative while also being fairly succinct.

## Conclusion

As we see here, it doesn't take many lines of code to write a very useful test assertion helper directly on top of the standard library, thanks to Go generics. In my opinion this covers 99% of Go unit testing needs. The remaining 1% is left as an exercise for the reader!