template/pkg/check/check.go

133 lines
3.3 KiB
Go

// Package check provides lightweight test helpers to reduce boilerplate in
// table-driven tests.
//
// Error-family functions (NoError, Error, ErrorContains) accept either a plain
// error or a result.Expect[T] value — the package extracts the error from
// whichever type it receives.
//
// Every helper calls t.Helper() so failures are reported at the call site.
package check
import (
"reflect"
"strings"
"testing"
"gitea.djmil.dev/go/template/pkg/result"
)
// extractErr pulls an error out of v, which must be error or result.Expect[T].
// Fatally fails the test for any other type.
func extractErr(t *testing.T, v any) error {
t.Helper()
if v == nil {
return nil
}
switch x := v.(type) {
case error:
return x
case interface{ Err() error }:
return x.Err()
default:
t.Fatalf("check: unsupported type %T (want error or result.Expect[T])", v)
return nil
}
}
// NoError fails the test if v contains a non-nil error.
func NoError(t *testing.T, v any) {
t.Helper()
if err := extractErr(t, v); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// Error fails the test if v contains no error.
func Error(t *testing.T, v any) {
t.Helper()
if extractErr(t, v) == nil {
t.Fatal("expected error, got nil")
}
}
// ErrorContains fails the test if v contains no error or its message does not
// contain substr.
func ErrorContains(t *testing.T, v any, substr string) {
t.Helper()
err := extractErr(t, v)
if err == nil {
t.Fatal("expected error, got nil")
return
}
if !strings.Contains(err.Error(), substr) {
t.Errorf("error %q does not contain %q", err.Error(), substr)
}
}
// Equal fails the test if got != want.
func Equal[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
// NotEqual fails the test if got == want.
func NotEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got == want {
t.Errorf("expected values to differ, got %v", got)
}
}
// DeepEqual fails the test if got and want are not deeply equal.
// Use instead of Equal for maps, slices, and structs with slice fields.
func DeepEqual(t *testing.T, got, want any) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
// ElementsMatch fails the test if got and want do not contain the same
// elements regardless of order, including duplicates. T must be comparable —
// for slices or maps as elements, use DeepEqual after sorting manually.
func ElementsMatch[T comparable](t *testing.T, got, want []T) {
t.Helper()
if len(got) != len(want) {
t.Errorf("length mismatch: got %d elements, want %d", len(got), len(want))
return
}
freq := make(map[T]int, len(want))
for _, v := range want {
freq[v]++
}
for _, v := range got {
if freq[v]--; freq[v] < 0 {
t.Errorf("unexpected element: %v", v)
return
}
}
}
// Ok fails the test if r holds an error, then returns the value.
func Ok[T any](t *testing.T, r result.Expect[T]) T {
t.Helper()
if r.Err() != nil {
t.Fatalf("unexpected error: %v", r.Err())
}
return r.Value()
}
// OkNotNil fails the test if r holds an error or its value is the zero value.
// T must be a pointer or comparable type where zero means absent.
func OkNotNil[T comparable](t *testing.T, r result.Expect[T]) T {
t.Helper()
v := Ok(t, r)
var zero T
if v == zero {
t.Fatal("expected non-nil value, got zero")
}
return v
}