diff options
Diffstat (limited to 'assert')
| -rw-r--r-- | assert/assert.go | 42 | ||||
| -rw-r--r-- | assert/equal.go | 16 | ||||
| -rw-r--r-- | assert/equal_test.go | 31 | ||||
| -rw-r--r-- | assert/errequal.go | 72 | ||||
| -rw-r--r-- | assert/errequal_test.go | 77 |
5 files changed, 238 insertions, 0 deletions
diff --git a/assert/assert.go b/assert/assert.go new file mode 100644 index 0000000..a13c15e --- /dev/null +++ b/assert/assert.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +// Package assert contains assertions for use in unit tests. +// Each assertion in this package returns a bool to indicate whether the check succeeded, and logs a t.Error() when the check does not succeed. +package assert + +import ( + "context" + "io" + "testing" +) + +// TestingTB contains all the public functions of [testing.TB] (as of Go 1.26). +// Functions in this package use this type instead of [testing.TB] because the capture device used by package testcapture cannot implement [testing.TB]: It contains methods that are private to the standard library. +type TestingTB interface { + ArtifactDir() string + Attr(key, value string) + Chdir(dir string) + Cleanup(func()) + Context() context.Context + Error(args ...any) + Errorf(format string, args ...any) + Fail() + Failed() bool + FailNow() + Fatal(args ...any) + Fatalf(format string, args ...any) + Helper() + Log(args ...any) + Logf(format string, args ...any) + Name() string + Output() io.Writer + Setenv(key, value string) + Skip(args ...any) + Skipf(format string, args ...any) + SkipNow() + Skipped() bool + TempDir() string +} + +var _ TestingTB = testing.TB(nil) diff --git a/assert/equal.go b/assert/equal.go new file mode 100644 index 0000000..1222ecf --- /dev/null +++ b/assert/equal.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package assert + +import "reflect" + +// Equal checks whether both supplied values are equal according to the rules of [reflect.DeepEqual]. +func Equal[V any](t TestingTB, actual, expected V) bool { + if reflect.DeepEqual(actual, expected) { + return true + } + t.Helper() + t.Errorf("expected %#v, but got %#v", expected, actual) + return false +} diff --git a/assert/equal_test.go b/assert/equal_test.go new file mode 100644 index 0000000..c710cea --- /dev/null +++ b/assert/equal_test.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package assert_test + +import ( + "strings" + "testing" + + "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/testcapture" +) + +func expectErrors(t *testing.T, test func(assert.TestingTB), expected string) { + t.Helper() + r := testcapture.Result{Outcome: testcapture.OutcomeFailed} + for line := range strings.SplitSeq(expected, "\n") { + line = strings.TrimSpace(line) + if line != "" { + r.Messages = append(r.Messages, testcapture.Log(line)) + } + } + assert.Equal(t, testcapture.Capture(t.Context(), t.Name(), test), r) +} + +func TestEqual(t *testing.T) { + assert.Equal(t, true, true) + expectErrors(t, func(t assert.TestingTB) { + assert.Equal(t, false, true) + }, `expected true, but got false`) +} diff --git a/assert/errequal.go b/assert/errequal.go new file mode 100644 index 0000000..4752f26 --- /dev/null +++ b/assert/errequal.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package assert + +import ( + "errors" + "fmt" + "reflect" + "regexp" +) + +// ErrEqual checks if the actual error matches the expectation. +// - If expected is nil, the actual error must be nil. +// - If expected is of type error, the actual error must be exactly equal to it or contain it, as reported by the [errors.Is] function. +// - If expected is of type string, the actual error must have a message exactly equal to it. +// - If expected is of type [*regexp.Regexp], the actual error must have a message matching that regexp. +// - If expected is of any other type, ErrEqual will panic. +func ErrEqual(t TestingTB, actual error, expected any) bool { + // coerce all types that implement `error` into the interface type `error`, + // and also coerce all nil values of concrete `error` types into untyped nil + if expectedErr, ok := expected.(error); ok { + // convert nil values of concrete error types into a generic nil value + if reflect.ValueOf(expectedErr).IsNil() { + expected = nil + } else { + expected = expectedErr + } + } + + switch expected := expected.(type) { + case nil: + if actual == nil { + return true + } else { + t.Errorf("expected no error, but got %q", actual.Error()) + return false + } + case error: + if actual == nil { + t.Errorf("expected %q, but got no error", expected.Error()) + return false + } else if errors.Is(actual, expected) { + return true + } else { + t.Errorf("expected %q, but got %q", expected.Error(), actual.Error()) + return false + } + case string: + if actual == nil { + t.Errorf("expected %q, but got no error", expected) + return false + } else if actual.Error() == expected { + return true + } else { + t.Errorf("expected %q, but got %q", expected, actual.Error()) + return false + } + case *regexp.Regexp: + if actual == nil { + t.Errorf("expected an error matching /%s/, but got no error", expected.String()) + return false + } else if expected.MatchString(actual.Error()) { + return true + } else { + t.Errorf("expected an error matching /%s/, but got %q", expected.String(), actual.Error()) + return false + } + default: + panic(fmt.Sprintf("cannot handle `expected` of type %T", expected)) + } +} diff --git a/assert/errequal_test.go b/assert/errequal_test.go new file mode 100644 index 0000000..5efbdc5 --- /dev/null +++ b/assert/errequal_test.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package assert_test + +import ( + "errors" + "fmt" + "os" + "regexp" + "testing" + + "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/testcapture" +) + +func TestErrEqual(t *testing.T) { + noError := error(nil) + fooError := errors.New("foo error") + barError := errors.New("bar error") + nestedFooError := fmt.Errorf("nested error: %w", fooError) + + // test matching against nil + assert.ErrEqual(t, noError, nil) + assert.ErrEqual(t, noError, (*os.PathError)(nil)) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, fooError, nil) + }, `expected no error, but got "foo error"`) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, fooError, (*os.PathError)(nil)) + }, `expected no error, but got "foo error"`) + + // test matching against error + assert.ErrEqual(t, fooError, fooError) + assert.ErrEqual(t, nestedFooError, fooError) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, fooError, nestedFooError) // error nesting does not work the other way, we need to see the full `expected`error in`actual` + }, `expected "nested error: foo error", but got "foo error"`) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, barError, fooError) // check with unrelated errors for completeness + }, `expected "foo error", but got "bar error"`) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, nil, fooError) + }, `expected "foo error", but got no error`) + + // test matching against string + assert.ErrEqual(t, fooError, "foo error") + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, nestedFooError, "foo error") // partial matches do not work + }, `expected "foo error", but got "nested error: foo error"`) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, barError, "foo error") // check with unrelated errors for completeness + }, `expected "foo error", but got "bar error"`) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, nil, "foo error") + }, `expected "foo error", but got no error`) + + // test matching against regexp + fooRegexp := regexp.MustCompile(`foo`) + assert.ErrEqual(t, fooError, fooRegexp) // partial matches allowed here as long as regexp does not use ^ and $ + assert.ErrEqual(t, nestedFooError, fooRegexp) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, barError, fooRegexp) // check with unrelated errors for completeness + }, `expected an error matching /foo/, but got "bar error"`) + expectErrors(t, func(t assert.TestingTB) { + assert.ErrEqual(t, nil, fooRegexp) + }, `expected an error matching /foo/, but got no error`) + + // test matching against unexpected type + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + assert.ErrEqual(t, errors.New("42"), 42) + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomePanicked, + Panic: "cannot handle `expected` of type int", + }) +} |
