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.
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:
- There are similar two packages (assert / require), depending if you want the assertion to stop the test or not
- It predates type parameters (generics), so all arguments are interface{}
- The API surface of each package is quite large (100s of methods, duplicated for the Assertions type which embeds testingT)
- Auto-completion is not nice (see below)
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:
- not stop the current test (you have to wrap t with be.Relaxed)
- log some additional context (be.Debug must be called before the assertion and will be printed after the failure)
My own take
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:
- check.Equal to compare comparable types
- check.Equals to compare slices of comparable types
- check.EqualDeep to compare other types (using reflect.DeepEqual)
And 3 type-methods:
- Fatal to stop the test execution on failure
- Log to log some context on failure (analogous to Println). Unexpected Printf formatting directives will be caught by the compiler.
- Logf to log some context on failure (analogous to Printf)
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:
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