💾 Archived View for log.pfad.fr › 2024 › reinventing-the-assertion-wheel captured on 2024-05-26 at 14:30:26. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Reinventing the Assertion Wheel (go test)

Like many developers, I like to reinvent the wheel. Like many Go developers, I created my own assertion package...

For a couple of months it lived as an internal package, but prompted by a discussion on lobste.rs, I finally decided to make it a dedicated package.

Why I don't use a third-party assertion library in Go unit tests | Lobsters

Third-party assertion libraries

Here is my experience using other assertion libraries

github.com/stretchr/testify

This is probably one of the first that I used. It is quite complete, but also quite complex:

github.com/matryer/is

This is (pun intended) very clever, but a bit too clever for my taste (looking up the comment of the assertion line to display it along the error message). It also predates type parameters, so interface{} everywhere.

gotest.tools/v3

I have been using this package until now. The API surface is reasonable but it also predates type parameters and does not play nicely with auto-completion.

github.com/carlmjohnson/be

The first package that leverages type parameters, however, I find it quite cumbersome to:

My own take

code.pfad.fr/check

This largely builds upon the "be" package above, with a twist: it returns a Failed struct to allow additional behavior on failure.

To mark a failure as fatal, call Fatal on the returned Failed struct (mainly interesting for error checking, to prevent a nil pointer later on):

obj, err := newObj()
check.Equal(t, nil, err).Fatal() // will stop the test on failure (by calling testing.FailNow)
check.Equal(t, obj, expected)    // the test continue, even on failure

To log additional context, call Log (or Logf) on the returned Failed struct:

check.Equal(t, a, b).
    Logf("context: %#v", c)

Both the Fatal and Log operations above are no-ops if the check succeeded. And you can call Fatal after a Log (but not the other way around since it wouldn't make sense to try to log something after the test got interrupted).

Plays nice with auto-completion

The most important part (for me, at least) is that this API plays nicely with auto-completion. With testify and gotest.tools the log arguments are parts of the main Equal method, meaning that my editor will automatically add placeholders to them (even if I don't use them 90% of the time). Here is what I get after letting my editor complete `assert.Equa`.

assert.Equal(t assert.TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{})

Note that the signature is different from Printf, while behaving the same way for msgAndArgs (to make them all optional, however the first string will be interpreted as a formatting string).

On the other end, with my package, I get (in both cases, typing [tab] allows me to cycle through the arguments):

check.Equal(t testing.TB, want T, got T)

And in the 10% of cases where I need some context, I can call Log(f) afterward.

Small API surface

The API consists of only 3 top-level methods:

And 3 type-methods:

Pluggable for more complex cases

In case you want to compare complex structs, there is check.EqualDeep. And if you want a nice diff, you can import github.com/google/go-cmp/cmp:

check.EqualDeep(t, expectedObj, obj).Log(cmp.Diff(expectedObj, obj))

For other examples, consult the documentation:

code.pfad.fr/check very minimal Go assertion package

If you don't want to add it as a dependency, feel free to copy/paste the check.go file into your project.

📅 2024-05-21

Back to the index

Send me a comment or feedback