DEV Community

Michael Kohl
Michael Kohl

Posted on • Originally published at citizen428.net on

Easy assertion helpers in Go

One of the things I enjoy about Go is its standard library, which comes with batteries included. Sure, some packages (like flag) aren't as powerful or ergonomic as the available alternatives but I'm generally willing to accept this in order to avoid dependencies.

Testing

The same also applies to Go's built-in test support, the testing package. Let's look at a typical test:

package example

import "testing"

func Add(a, b int) int {
    return a + b
}

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

    if got != want {
        t.Errorf("got %v, wanted %v", got, want)
    }
}
Enter fullscreen mode Exit fullscreen mode

This is not too bad but gets a bit repetitive, so people often end up using external assertion libraries like testify. However, for most of my projects I try to avoid external dependencies as much as possible, because I don't want to deal with upgrading them and similar headaches. Today it struck me that the generics (added in Go 1.18) make it easy to write useful assertion helpers without resorting to any libraries. Here's a helper I added to one of my projects today:

func assertEqual[T comparable](t *testing.T, got, want T) {
    t.Helper()

    if got != want {
        t.Errorf("got %v, wanted %v", got, want)
    }
}
Enter fullscreen mode Exit fullscreen mode

This equality assertion takes a type parameter T, which will be satisfied by any type satisfying the comparable interface defined in the universe block. Additionally it takes three positional parameters: the test case as well as the actual and desired values. The t.Helper() method ensures that failed assertions will be reported for the line where assertEqual is used, not the line inside the test helper function.

This is what the test looks like now:

package example

import "testing"

func Add(a, b int) int {
    return a + b
}

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

    assertEqual(t, got, want)

    // or simply
    // assertEqual(t, Add(4, 2), 6)
}
Enter fullscreen mode Exit fullscreen mode

Not a big deal for a single test but noticeably nicer across an entire test suite.

Summary

Using Go's new generics allowed me to refactor and simplify the test suite of a moderately complex project without having to add any external dependencies.

Discussion (5)

Collapse
marcello_h profile image
Marcelloh

A nice example on generics :-)

Collapse
citizen428 profile image
Michael Kohl Author • Edited on

Thanks!

I'm generally trying to avoid the new and shiny thing but this really was an easy way to clean up the most repetitive part of testing with the standard library. I still resisted the temptation of adding a type switch on T to e.g. use reflect.DeepEqual for slices, I only have like two tests that'd need it in this suite and I can live with that bit of duplication.

Collapse
quenbyako profile image
Richard Cooper

That looks better, than interfaces in testify package 👍
Maybe it's time to make proposal? 🤔🤔🤔

Collapse
maxatome profile image
Maxime Soulé

Hi, it is nice, but once you start to compare complex data structure you don't only want to know that the 2 structs differ, but where is exactly the difference.
Moreover it works only for comparable items, not slices for example. So in this case you need to rely on reflect.DeepEqual with the same problem: where is the difference?
Last but not least, you sometimes want to check a value is different from another, or greater than, or included between 2 values… This value can be nested inside a complex structure.
IMHO it is too bad to limit your testing approach and duplicate code (even small) from repos to repos just to avoid a testing dependency providing a huge number of features heavily tested.
Disclaimer: go-testdeep author here.

Collapse
citizen428 profile image
Michael Kohl Author • Edited on

I mentioned part of that in another comment. The entire test suite of this project (an emulator) had 2 tests that needed reflect.DeepEqual. I’m more than happy to keep these two tests a little bit more verbose to avoid a dependency. Most of my tests just need equality assertions on emulated RAM and registers. YMMV.

In a different project, especially one that already has other dependencies, I’d probably just go ahead and use Testify or Gomega or whatever. I just don’t like reaching for third party code as the default.

Thanks for your work btw, go-testdeep is a great library 👍