Skip to content

Repository files navigation

test

LicenseGo ReferenceGo Report CardGitHubCIcodecov

A lightweight test helper package 🧪

Project Description

test is my take on a handy, lightweight Go test helper package. Inspired by matryer/is, earthboundkid/be and others.

It provides a lightweight, but useful, extension to the std lib testing package with a friendlier and hopefully intuitive API. You definitely don't need it, but might find it useful anyway 🙂

Installation

go get go.followtheprocess.codes/test@latest

Usage

test is as easy as...

funcTestSomething(t*testing.T) {
test.Equal(t, "hello", "hello") // Obviously finetest.Equal(t, "hello", "there") // Failstest.NotEqual(t, 42, 27) // Passes, these are not equaltest.NotEqual(t, 42, 42) // Failstest.NearlyEqual(t, 3.0000000001, 3.0) // Look, floats handled easily!err:=doSomething()
test.Ok(t, err) // Fails if err != niltest.Err(t, err) // Fails if err == niltest.True(t, true) // Passestest.False(t, true) // Fails
}

Add Additional Context

test provides a number of options to decorate your test log with useful context:

funcTestDetail(t*testing.T) {
test.Equal(t, "apples", "oranges", test.Title("Fruit scramble!"), test.Context("Apples are not oranges!"))
}

Will get you an error log in the test that looks like this...

--- FAIL: TestDemo (0.00s)
test_test.go:501:
Fruit scramble!
---------------
Got: apples
Wanted: oranges
(Apples are not oranges!)
FAIL

Non Comparable Types

test uses generics under the hood for most of the comparison, which is great, but what if your types don't satisfy comparable. We also provide test.EqualFunc and test.NotEqualFunc for those exact situations!

These allow you to pass in a custom comparator function for your type, if your comparator function returns true, the types are considered equal.

funcTestNonComparableTypes(t*testing.T) {
// Slices do not satisfy comparablea:= []string{"hello", "there"}
b:= []string{"hello", "there"}
c:= []string{"general", "kenobi"}
// Custom function, returns true if things should be considered equalsliceEqual:=func(a, b, []string) { returntrue } // Cheatingtest.EqualFunc(t, a, b, sliceEqual) // Passes// Can also use any function heretest.EqualFunc(t, a, b, slices.Equal) // Also passes :)test.EqualFunc(t, a, c, slices.Equal) // Fails
}

You can also use this same pattern for custom user defined types, structs etc.

Table Driven Tests

Table driven tests are great! But when you test errors too it can get a bit awkward, you have to do the if (err != nil) != tt.wantErr thing and I personally always have to do the boolean logic in my head to make sure I got that right. Enter test.WantErr:

funcTestTableThings(t*testing.T) {
tests:= []struct {
namestringwantintwantErrbool
}{
{
name: "no error",
want: 4,
wantErr: false,
},
{
name: "yes error",
want: 4,
wantErr: true,
},
}
for_, tt:=rangetests {
t.Run(tt.name, func(t*testing.T) {
got, err:=SomeFunction()
test.WantErr(t, err, tt.wantErr)
test.Equal(t, got, tt.want)
})
}
}

Which is basically semantically equivalent to:

funcTestTableThings(t*testing.T) {
tests:= []struct {
namestringwantintwantErrbool
}{
{
name: "no error",
want: 4,
wantErr: false,
},
{
name: "yes error",
want: 4,
wantErr: true,
},
}
for_, tt:=rangetests {
t.Run(tt.name, func(t*testing.T) {
got, err:=SomeFunction()
iftt.wantErr {
test.Err(t, err)
} else {
test.Ok(t, err)
}
test.Equal(t, got, tt.want)
})
}
}

Capturing Stdout and Stderr

We've all been there, trying to test a function that prints but doesn't accept an io.Writer as a destination 🙄.

That's where test.CaptureOutput comes in!

funcTestOutput(t*testing.T) {
// Function that prints to stdout and stderr, but imagine this is defined somewhere else// maybe a 3rd party library that you don't control, it just prints and you can't tell it wherefn:=func() error {
fmt.Fprintln(os.Stdout, "hello stdout")
fmt.Fprintln(os.Stderr, "hello stderr")
returnnil
}
// CaptureOutput to the rescue!stdout, stderr:=test.CaptureOutput(t, fn)
test.Equal(t, stdout, "hello stdout\n")
test.Equal(t, stderr, "hello stderr\n")
}

Under the hood CaptureOutput temporarily captures both streams, copies the data to a buffer and returns the output back to you, before cleaning everything back up again.

A note on ErrorAs and errcheck

test.ErrorAs[T] returns the matched error so you can chain further assertions on its fields:

got:=test.ErrorAs[*os.PathError](t, err)
test.Equal(t, got.Op, "open")

The return value is optional — discarding it is a supported pattern when you only want the type-check assertion:

test.ErrorAs[*os.PathError](t, err) // pure type check, return ignored

If you lint with errcheck it will flag the discard because T is constrained to error. errcheck's exclude-functions doesn't currently match generic instantiations, so the cleanest fix is a source-based exclusion in .golangci.yml:

linters:
exclusions:
rules:
- source: 'test\.ErrorAs\['linters:
- errcheck

See Also

Credits

This package was created with copier and the FollowTheProcess/go_copier project template.

About

A lightweight test helper package 🧪

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages