Skip to content

Commit

Permalink
feat: add Panics (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmzane authored Jul 24, 2023
1 parent 7cf0436 commit e29fa65
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 49 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ Asserts that `errors.As(err, target)` is true.
assert.AsErr[E](t, err, new(*os.PathError))
```

### Panics

Asserts that the given function panics with the argument `v`.

```go
assert.Panics[E](t, func() { /* panic? */ }, 42)
```

## ❤️ Credits

Inspired by [`matryer/is`][2].
Expand Down
19 changes: 17 additions & 2 deletions assert.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func NoErr[T Param](t TB, err error, formatAndArgs ...any) {
func IsErr[T Param](t TB, err, target error, formatAndArgs ...any) {
t.Helper()
if !errors.Is(err, target) {
fail[T](t, formatAndArgs, "errors.Is == false\ngot: %v\nwant: %v", err, target)
fail[T](t, formatAndArgs, "errors.Is() mismatch\ngot: %v\nwant: %v", err, target)
}
}

Expand All @@ -58,10 +58,25 @@ func AsErr[T Param](t TB, err error, target any, formatAndArgs ...any) {
t.Helper()
if !errors.As(err, target) {
typ := reflect.TypeOf(target).Elem() // dereference the pointer to get the real type.
fail[T](t, formatAndArgs, "errors.As == false\ngot: %T\nwant: %s", err, typ)
fail[T](t, formatAndArgs, "errors.As() mismatch\ngot: %T\nwant: %s", err, typ)
}
}

// Panics asserts that the given function panics with the argument v.
func Panics[T Param](t TB, fn func(), v any, formatAndArgs ...any) {
t.Helper()
defer func() {
t.Helper()
switch r := recover(); {
case r == nil:
fail[T](t, formatAndArgs, "the function didn't panic")
case !reflect.DeepEqual(r, v):
fail[T](t, nil, "panic argument mismatch\ngot: %v\nwant: %v", r, v)
}
}()
fn()
}

// fail marks the test as having failed and continues/stops its execution based on T's type.
func fail[T Param](t TB, customFormatAndArgs []any, format string, args ...any) {
t.Helper()
Expand Down
106 changes: 64 additions & 42 deletions assert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,23 @@ func TestEqual(t *testing.T) {
"ok [E]": {
fn: assert.Equal[E, int],
a: 1, b: 1,
want: okCall(),
want: assertCall{helperCalls: 1},
},
"fail [E]": {
fn: assert.Equal[E, int],
a: 1, b: 2,
want: errorfCall("values are not equal\ngot: 1\nwant: 2"),
want: assertCall{helperCalls: 2, errorfCalled: true, message: "values are not equal\ngot: 1\nwant: 2"},
},
"fail [F]": {
fn: assert.Equal[F, int],
a: 1, b: 2,
want: fatalfCall("values are not equal\ngot: 1\nwant: 2"),
want: assertCall{helperCalls: 2, fatalfCalled: true, message: "values are not equal\ngot: 1\nwant: 2"},
},
"fail [F] (custom message)": {
fn: assert.Equal[F, int],
a: 1, b: 2,
formatAndArgs: []any{"%d != %d", 1, 2},
want: fatalfCall("1 != 2"),
want: assertCall{helperCalls: 2, fatalfCalled: true, message: "1 != 2"},
},
}

Expand All @@ -57,23 +57,23 @@ func TestNoErr(t *testing.T) {
"ok [E]": {
fn: assert.NoErr[E],
err: nil,
want: okCall(),
want: assertCall{helperCalls: 1},
},
"fail [E]": {
fn: assert.NoErr[E],
err: errFoo,
want: errorfCall("unexpected error: foo"),
want: assertCall{helperCalls: 2, errorfCalled: true, message: "unexpected error: foo"},
},
"fail [F]": {
fn: assert.NoErr[F],
err: errFoo,
want: fatalfCall("unexpected error: foo"),
want: assertCall{helperCalls: 2, fatalfCalled: true, message: "unexpected error: foo"},
},
"fail [F] (custom message)": {
fn: assert.NoErr[F],
err: errFoo,
formatAndArgs: []any{"%v != nil", errFoo},
want: fatalfCall("foo != nil"),
want: assertCall{helperCalls: 2, fatalfCalled: true, message: "foo != nil"},
},
}

Expand All @@ -98,26 +98,26 @@ func TestIsErr(t *testing.T) {
fn: assert.IsErr[E],
err: errFoo,
target: errFoo,
want: okCall(),
want: assertCall{helperCalls: 1},
},
"fail [E]": {
fn: assert.IsErr[E],
err: errFoo,
target: errBar,
want: errorfCall("errors.Is == false\ngot: foo\nwant: bar"),
want: assertCall{helperCalls: 2, errorfCalled: true, message: "errors.Is() mismatch\ngot: foo\nwant: bar"},
},
"fail [F]": {
fn: assert.IsErr[F],
err: errFoo,
target: errBar,
want: fatalfCall("errors.Is == false\ngot: foo\nwant: bar"),
want: assertCall{helperCalls: 2, fatalfCalled: true, message: "errors.Is() mismatch\ngot: foo\nwant: bar"},
},
"fail [F] (custom message)": {
fn: assert.IsErr[F],
err: errFoo,
target: errBar,
formatAndArgs: []any{"%v != %v", errFoo, errBar},
want: fatalfCall("foo != bar"),
want: assertCall{helperCalls: 2, fatalfCalled: true, message: "foo != bar"},
},
}

Expand All @@ -142,26 +142,26 @@ func TestAsErr(t *testing.T) {
fn: assert.AsErr[E],
err: errFoo,
target: new(fooError),
want: okCall(),
want: assertCall{helperCalls: 1},
},
"fail [E]": {
fn: assert.AsErr[E],
err: errFoo,
target: new(barError),
want: errorfCall("errors.As == false\ngot: assert_test.fooError\nwant: assert_test.barError"),
want: assertCall{helperCalls: 2, errorfCalled: true, message: "errors.As() mismatch\ngot: assert_test.fooError\nwant: assert_test.barError"},
},
"fail [F]": {
fn: assert.AsErr[F],
err: errFoo,
target: new(barError),
want: fatalfCall("errors.As == false\ngot: assert_test.fooError\nwant: assert_test.barError"),
want: assertCall{helperCalls: 2, fatalfCalled: true, message: "errors.As() mismatch\ngot: assert_test.fooError\nwant: assert_test.barError"},
},
"fail [F] (custom message)": {
fn: assert.AsErr[F],
err: errFoo,
target: new(barError),
formatAndArgs: []any{"%T != %T", errFoo, errBar},
want: fatalfCall("assert_test.fooError != assert_test.barError"),
want: assertCall{helperCalls: 2, fatalfCalled: true, message: "assert_test.fooError != assert_test.barError"},
},
}

Expand All @@ -174,6 +174,50 @@ func TestAsErr(t *testing.T) {
}
}

func TestPanics(t *testing.T) {
tests := map[string]struct {
fn func(t assert.TB, fn func(), v any, formatAndArgs ...any)
panicFn func()
v any
formatAndArgs []any
want assertCall
}{
"ok [E]": {
fn: assert.Panics[E],
panicFn: func() { panic(42) },
v: 42,
want: assertCall{helperCalls: 2},
},
"fail [E] (didn't panic)": {
fn: assert.Panics[E],
panicFn: func() {},
v: 42,
want: assertCall{helperCalls: 3, errorfCalled: true, message: "the function didn't panic"},
},
"fail [F] (unexpected argument)": {
fn: assert.Panics[F],
panicFn: func() { panic(41) },
v: 42,
want: assertCall{helperCalls: 3, fatalfCalled: true, message: "panic argument mismatch\ngot: 41\nwant: 42"},
},
"fail [F] (custom message)": {
fn: assert.Panics[F],
panicFn: func() {},
v: 42,
formatAndArgs: []any{"no panic occured"},
want: assertCall{helperCalls: 3, fatalfCalled: true, message: "no panic occured"},
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
var got assertCall
tt.fn(&got, tt.panicFn, tt.v, tt.formatAndArgs...)
testAssertCall(t, got, tt.want)
})
}
}

type assertCall struct {
helperCalls int
errorfCalled bool
Expand All @@ -193,41 +237,19 @@ func (ac *assertCall) Fatalf(format string, args ...any) {
ac.message = fmt.Sprintf(format, args...)
}

func okCall() assertCall {
return assertCall{
helperCalls: 1, // at least one t.Helper() call is always expected.
}
}

func errorfCall(message string) assertCall {
return assertCall{
helperCalls: 2,
errorfCalled: true,
message: message,
}
}

func fatalfCall(message string) assertCall {
return assertCall{
helperCalls: 2,
fatalfCalled: true,
message: message,
}
}

func testAssertCall(t *testing.T, got, want assertCall) {
t.Helper()
if got.helperCalls != want.helperCalls {
t.Errorf("t.Helper() calls: got %d want %d", got.helperCalls, want.helperCalls)
t.Errorf("t.Helper() calls mismatch\ngot: %d\nwant: %d", got.helperCalls, want.helperCalls)
}
if got.errorfCalled != want.errorfCalled {
t.Errorf("t.Errorf() called: got %t want %t", got.errorfCalled, want.errorfCalled)
t.Errorf("t.Errorf() called mismatch\ngot: %t\nwant: %t", got.errorfCalled, want.errorfCalled)
}
if got.fatalfCalled != want.fatalfCalled {
t.Errorf("t.Fatalf() called: got %t want %t", got.fatalfCalled, want.fatalfCalled)
t.Errorf("t.Fatalf() called mismatch\ngot: %t\nwant: %t", got.fatalfCalled, want.fatalfCalled)
}
if got.message != want.message {
t.Errorf("unexpected message\ngot: %q\nwant: %q", got.message, want.message)
t.Errorf("message mismatch\ngot: %q\nwant: %q", got.message, want.message)
}
}

Expand Down
17 changes: 12 additions & 5 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,31 @@ func ExampleEqual() {
}

func ExampleNoErr() {
assert.NoErr[E](t, os.ErrExist)
assert.NoErr[E](t, err)
// Output: unexpected error: file already exists
}

func ExampleIsErr() {
assert.IsErr[E](t, os.ErrExist, os.ErrNotExist)
// Output: errors.Is == false
assert.IsErr[E](t, err, os.ErrNotExist)
// Output: errors.Is() mismatch
// got: file already exists
// want: file does not exist
}

func ExampleAsErr() {
assert.AsErr[E](t, os.ErrExist, new(*os.PathError))
// Output: errors.As == false
assert.AsErr[E](t, err, new(*os.PathError))
// Output: errors.As() mismatch
// got: *errors.errorString
// want: *fs.PathError
}

func ExamplePanics() {
assert.Panics[E](t, func() { /* panic? */ }, 42)
// Output: the function didn't panic
}

var err = os.ErrExist

var t printer

type printer struct{}
Expand Down

0 comments on commit e29fa65

Please sign in to comment.