From e29fa65df12953c8997eb98e8f9a5de87cd37f6c Mon Sep 17 00:00:00 2001 From: Tom <73077675+tmzane@users.noreply.github.com> Date: Mon, 24 Jul 2023 22:29:48 +0300 Subject: [PATCH] feat: add `Panics` (#11) --- README.md | 8 ++++ assert.go | 19 ++++++++- assert_test.go | 106 +++++++++++++++++++++++++++++------------------- example_test.go | 17 +++++--- 4 files changed, 101 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 3e82714..0a86ff9 100644 --- a/README.md +++ b/README.md @@ -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]. diff --git a/assert.go b/assert.go index be84d19..6968ab8 100644 --- a/assert.go +++ b/assert.go @@ -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) } } @@ -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() diff --git a/assert_test.go b/assert_test.go index 99bf800..a96b19d 100644 --- a/assert_test.go +++ b/assert_test.go @@ -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"}, }, } @@ -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"}, }, } @@ -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"}, }, } @@ -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"}, }, } @@ -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 @@ -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) } } diff --git a/example_test.go b/example_test.go index 2837a1f..fb7e2d2 100644 --- a/example_test.go +++ b/example_test.go @@ -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{}