Skip to content

Commit

Permalink
Refactor to improve error descriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
skhome committed Jan 15, 2024
1 parent 1bc24fb commit e3feb15
Show file tree
Hide file tree
Showing 20 changed files with 1,745 additions and 1,641 deletions.
34 changes: 20 additions & 14 deletions assert/assert.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
package assert

// TestingT is an interface wrapper for *testing.T
type TestingT interface {
Errorf(format string, args ...any)
}

// Predicate is a function that returns if a value meets a condition.
type Predicate[T any] func(value T) bool

// Condition is a function to assert a condition.
type Condition[T any] func(value T)

// ThatString starts assertions on a string.
func ThatString(t TestingT, actual string) *StringAssert {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return &StringAssert{t: t, actual: actual}
stringAssert := &StringAssert{actual: actual}
baseAssert := &BaseAssert[StringAssert]{t: t, info: NewWritableAssertionInfo(), a: stringAssert}
stringAssert.BaseAssert = baseAssert
return stringAssert
}

// ThatSlice starts assertions on a slice.
func ThatSlice[E any](t TestingT, actual []E) *SliceAssert[E] {
func ThatSlice[T ~[]E, E any](t TestingT, actual T) *SliceAssert[E] {
if h, ok := t.(tHelper); ok {
h.Helper()
}
sliceAssert := &SliceAssert[E]{actual: actual}
baseAssert := &BaseAssert[SliceAssert[E]]{t: t, info: NewWritableAssertionInfo(), a: sliceAssert}
sliceAssert.BaseAssert = baseAssert
return sliceAssert
}

// ThatBool starts assertions on a bool.
func ThatBool(t TestingT, actual bool) *BoolAssert {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return &SliceAssert[E]{t: t, actual: actual}
boolAssert := &BoolAssert{actual: actual}
baseAssert := &BaseAssert[BoolAssert]{t: t, info: NewWritableAssertionInfo(), a: boolAssert}
boolAssert.BaseAssert = baseAssert
return boolAssert
}
67 changes: 67 additions & 0 deletions assert/assert_base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package assert

type BaseAssert[T any] struct {
t TestingT
info *WritableAssertionInfo
a *T
}

// DescribedAs sets an optional description for the following assertion.
func (a *BaseAssert[T]) DescribedAs(description Description) *T {
a.info.WithDescription(description)
return a.a
}

// WithFailMessage overrides the default error message for the following assertions.
func (a *BaseAssert[T]) WithFailMessage(message string, args ...any) *T {
a.info.WithOverridingFailureMessage(message, args...)
return a.a
}

// WithFailMessageSupplier overrides the default error message for the following assertions.
// The new error message is built if the assertion fails by consuming the given Supplier function.
func (a *BaseAssert[T]) WithFailMessageSupplier(supplier Supplier[string]) *T {
a.info.WithOverridingFailureMessageSupplier(supplier)
return a.a
}

// WithRepresentation uses the given representation to describe values in error messages.
//
// currencyRepresentation := func(value any) string { return fmt.Sprintf("€%d", value) }
//
// // assertion will fail with message:
// // expected value to be zero, but got €42
// assert.ThatInt(t, 42).
// WithRepresentation(currencyRepresentation).
// IsZero()
func (a *BaseAssert[T]) WithRepresentation(representation Representation) *T {
a.info.UsingRepresentation(representation)
return a.a
}

// InHexadecimal uses hexadecimal representation to describe values in error messages.
func (a *BaseAssert[T]) InHexadecimal() *T {
a.info.UsingHexadecimalRepresentation()
return a.a
}

// InBinary uses binary representation to describe values in error messages.
func (a *BaseAssert[T]) InBinary() *T {
a.info.UsingBinaryRepresentation()
return a.a
}

// FailWithMessage records an assertion error
func (a *BaseAssert[T]) FailWithMessage(message string, args ...any) {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
description := a.info.Description()
overridingErrorMessage := a.info.OverridingFailureMessage()
representation := a.info.Representation()
if overridingErrorMessage != "" {
a.t.Errorf(messageFormatter.Format(description, representation, overridingErrorMessage, args...))
} else {
a.t.Errorf(messageFormatter.Format(description, representation, message, args...))
}
}
94 changes: 94 additions & 0 deletions assert/assert_base_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package assert

import (
"fmt"
"strings"
"testing"
)

type fixtureT struct {
message string
}

func (f *fixtureT) Errorf(format string, args ...any) {
f.message = fmt.Sprintf(format, args...)
}

func (f *fixtureT) Helper() {}

type DummyAssert struct {
*BaseAssert[DummyAssert]
}

func TestDescription(t *testing.T) {
fixture := new(fixtureT)
baseAssert := &BaseAssert[DummyAssert]{t: fixture, info: NewWritableAssertionInfo()}

baseAssert.DescribedAs("hobbit")
baseAssert.FailWithMessage("message")

if !strings.Contains(fixture.message, "[hobbit]") {
t.Errorf("expected assertion error message to contain description %s, but got %s", "[hobbit]", fixture.message)
}
}

func TestOverridingFailMessage(t *testing.T) {
fixture := new(fixtureT)
baseAssert := &BaseAssert[DummyAssert]{t: fixture, info: NewWritableAssertionInfo()}

baseAssert.WithFailMessage("overriding message")
baseAssert.FailWithMessage("default message")

if !strings.Contains(fixture.message, "overriding message") {
t.Errorf("expected assertion error message to contain %s, but got %s", "overriding message", fixture.message)
}
}

func TestOverridingFailMessageSupplier(t *testing.T) {
fixture := new(fixtureT)
baseAssert := &BaseAssert[DummyAssert]{t: fixture, info: NewWritableAssertionInfo()}

baseAssert.WithFailMessageSupplier(func() string { return "overriding message" })
baseAssert.FailWithMessage("default message")

if !strings.Contains(fixture.message, "overriding message") {
t.Errorf("expected assertion error message to contain %s, but got %s", "overriding message", fixture.message)
}
}

func TestRepresentation(t *testing.T) {
fixture := new(fixtureT)
baseAssert := &BaseAssert[DummyAssert]{t: fixture, info: NewWritableAssertionInfo()}

currencyRepresentation := func(value any) string { return fmt.Sprintf("€%d", value) }
baseAssert.WithRepresentation(currencyRepresentation)
baseAssert.FailWithMessage("expected %s", 42)

if !strings.Contains(fixture.message, "€42") {
t.Errorf("expected assertion error message to contain %s, but got %s", "€42", fixture.message)
}
}

func TestHexadecimalRepresentation(t *testing.T) {
fixture := new(fixtureT)
baseAssert := &BaseAssert[DummyAssert]{t: fixture, info: NewWritableAssertionInfo()}

baseAssert.InHexadecimal()
baseAssert.FailWithMessage("expected %s", 42)

if !strings.Contains(fixture.message, "2A") {
t.Errorf("expected assertion error message to contain %s, but got %s", "2A", fixture.message)
}
}

func TestBinaryRepresentation(t *testing.T) {
fixture := new(fixtureT)
baseAssert := &BaseAssert[DummyAssert]{t: fixture, info: NewWritableAssertionInfo()}

baseAssert.InBinary()
baseAssert.FailWithMessage("expected %s", 42)

if !strings.Contains(fixture.message, "101010") {
t.Errorf("expected assertion error message to contain %s, but got %s", "101010", fixture.message)
}
}
75 changes: 75 additions & 0 deletions assert/assert_bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package assert

// BoolAssert provides assertions on boolean values.
type BoolAssert struct {
*BaseAssert[BoolAssert]
actual bool
}

// IsTrue verifies that the actual value is true.
//
// // assertions will pass
// assert.ThatBool(t, true).IsTrue()
//
// // assertions will fail
// assert.ThatBool(t, false).IsTrue()
func (a *BoolAssert) IsTrue() *BoolAssert {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
if !a.actual {
a.FailWithMessage("expected value to be true, but got %s", a.actual)
}
return a
}

// IsFalse verifies that the actual value is true.
//
// // assertions will pass
// assert.ThatBool(t, false).IsFalse()
//
// // assertions will fail
// assert.ThatBool(t, true).IsFalse()
func (a *BoolAssert) IsFalse() *BoolAssert {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
if a.actual {
a.FailWithMessage("expected value to be false, but got %s", a.actual)
}
return a
}

// IsEqualTo verifies that the actual value is equal to the given one.
//
// // assertions will pass
// assert.ThatBool(t, false).IsEqualTo(false)
//
// // assertions will fail
// assert.ThatBool(t, true).IsEqualTo(false)
func (a *BoolAssert) IsEqualTo(expected bool) *BoolAssert {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
if a.actual {
a.FailWithMessage("expected value to be %s, but got %s", expected, a.actual)
}
return a
}

// IsNotEqualTo verifies that the actual value is not equal to the given one.
//
// // assertions will pass
// assert.ThatBool(t, false).IsNotEqualTo(true)
//
// // assertions will fail
// assert.ThatBool(t, true).IsNotEqualTo(true)
func (a *BoolAssert) IsNotEqualTo(expected bool) *BoolAssert {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
if a.actual {
a.FailWithMessage("expected value not to be %s, but got %s", expected, a.actual)
}
return a
}
62 changes: 62 additions & 0 deletions assert/assert_bool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package assert_test

import (
"fmt"
"testing"

"github.com/skhome/assertg/assert"
)

type boolTest struct {
value bool
expected bool
ok bool
}

func TestBoolIsTrue(t *testing.T) {
tests := []boolTest{
{value: true, ok: true},
{value: false, ok: false},
}
messageFormat := "expected value to be true, but got <%t>"
runTests(t, tests)(func(fixture *fixtureT, test boolTest) (bool, string) {
assert.ThatBool(fixture, test.value).IsTrue()
return test.ok, fmt.Sprintf(messageFormat, test.value)
})
}

func TestBoolIsFalse(t *testing.T) {
tests := []boolTest{
{value: false, ok: true},
{value: true, ok: false},
}
messageFormat := "expected value to be false, but got <%t>"
runTests(t, tests)(func(fixture *fixtureT, test boolTest) (bool, string) {
assert.ThatBool(fixture, test.value).IsFalse()
return test.ok, fmt.Sprintf(messageFormat, test.value)
})
}

func TestBoolIsEqualTo(t *testing.T) {
tests := []boolTest{
{value: false, expected: false, ok: true},
{value: true, expected: false, ok: false},
}
messageFormat := "expected value to be <%t>, but got <%t>"
runTests(t, tests)(func(fixture *fixtureT, test boolTest) (bool, string) {
assert.ThatBool(fixture, test.value).IsEqualTo(test.expected)
return test.ok, fmt.Sprintf(messageFormat, test.expected, test.value)
})
}

func TestBoolIsNotEqualTo(t *testing.T) {
tests := []boolTest{
{value: false, expected: true, ok: true},
{value: true, expected: true, ok: false},
}
messageFormat := "expected value not to be <%t>, but got <%t>"
runTests(t, tests)(func(fixture *fixtureT, test boolTest) (bool, string) {
assert.ThatBool(fixture, test.value).IsNotEqualTo(test.expected)
return test.ok, fmt.Sprintf(messageFormat, test.expected, test.value)
})
}
Loading

0 comments on commit e3feb15

Please sign in to comment.