Concise test assertions with Be

I appreciate Go's verbosity, but not when it comes to writing tests. Endless if statements with t.Errorf not only make tests long, but also obscure their purpose and make them harder to read. For test assertions, I'd prefer something more concise than what the standard library offers.

There's testify/assert, of course. It's a decent package, but it's quite big. There are about 40 assertion functions, and that's just the tip of the iceberg. Definitely not my thing.

There's also Mat Ryer's is. It's about as minimal as a package can get, but its focus is a bit puzzling to me. Instead of mainly helping with assertions, it seems to concentrate more on parsing comments and formatting the output.

I wanted a small package, about the size of is, with decent assertion features for testing equality and errors. So I made one.

Be — a minimal test assertions package

Be is a simple test assertions package that focuses on equality checks and flexible error checking. Here are a few highlights:

  • Minimal API: Equal, Err, and True assertions.
  • Correctly compares time.Time values and other types with an Equal method.
  • Flexible error assertions: check if an error exists, check its value, type, or any combination of these.

Equal asserts that two values are equal:

func Test(t *testing.T) {
    t.Run("pass", func(t *testing.T) {
        got, want := "hello", "hello"
        be.Equal(t, got, want)
        // ok
    })

    t.Run("fail", func(t *testing.T) {
        got, want := "olleh", "hello"
        be.Equal(t, got, want)
        // want "hello", got "olleh"
    })
}

Err asserts that there is an error:

func Test(t *testing.T) {
    _, err := regexp.Compile("he(?o") // invalid
    be.Err(t, err)
    // ok
}

Or that there are no errors:

func Test(t *testing.T) {
    _, err := regexp.Compile("he??o") // valid
    be.Err(t, err, nil)
    // ok
}

Or that an error message contains a substring:

func Test(t *testing.T) {
    _, err := regexp.Compile("he(?o") // invalid
    be.Err(t, err, "invalid or unsupported")
    // ok
}

Or that an error matches the expected error according to errors.Is:

func Test(t *testing.T) {
    err := &fs.PathError{
        Op: "open",
        Path: "file.txt",
        Err: fs.ErrNotExist,
    }
    be.Err(t, err, fs.ErrNotExist)
    // ok
}

Or that the error type matches the expected type according to errors.As:

func Test(t *testing.T) {
    got := &fs.PathError{
        Op: "open",
        Path: "file.txt",
        Err: fs.ErrNotExist,
    }
    be.Err(t, got, reflect.TypeFor[*fs.PathError]())
    // ok
}

Or a mix of the above:

func Test(t *testing.T) {
    err := AppError("oops")
    be.Err(t, err,
        "failed",
        AppError("oops"),
        reflect.TypeFor[AppError](),
    )
    // ok
}

True asserts that an expression is true:

func Test(t *testing.T) {
    s := "go is awesome"
    be.True(t, strings.Contains(s, "go"))
    // ok
}

That's it!

Design decisions

Be is opinionated. It only has three assert functions, which are perfectly enough to write good tests.

Unlike other testing packages, be doesn't support custom error messages. When a test fails, you'll end up checking the code anyway, so why bother? The line number shows the way.

Be has flexible error assertions. You don't need to choose between Error, ErrorIs, ErrorAs, ErrorContains, NoError, or anything like that — just use be.Err. It covers everything.

Be doesn't fail the test when an assertion fails, so you can see all the errors at once instead of hunting them one by one. The only exception is when the be.Err(err, nil) assertion fails — this means there was an unexpected error. In this case, the test terminates immediately because any following assertions probably won't make sense and could cause panics.

The parameter order is (got, want), not (want, got). It just feels more natural — like saying "account balance is 100 coins" instead of "100 coins is the account balance".

Be currently has ≈100 lines of code (+450 lines for tests). For comparison, is has ≈250 loc (+250 lines for tests).

Final thoughts

If you think the standard library's approach is too wordy, but still want to keep things simple, try using be! You can install it with go get github.com/nalgeon/be and get started right away.

I hope it serves you well.

See the nalgeon/be repo if you are interested.

★ Subscribe to keep up with new posts.