Skip to content

Commit

Permalink
add initial assertion library
Browse files Browse the repository at this point in the history
  • Loading branch information
MKrupauskas authored and jeffbean committed Aug 19, 2024
1 parent 27bc0d6 commit 6d30b8b
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 0 deletions.
51 changes: 51 additions & 0 deletions internal/assert/assert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Internal assertion library inspired by https://github.com/stretchr/testify.
// "A little copying is better than a little dependency." - https://go-proverbs.github.io.
// We don't want the library to have dependencies, so we write our own assertions.
package assert

import (
"fmt"
"reflect"
)

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

// Equal asserts that expected is equal to actual.
func Equal(t TestingT, want, got interface{}, msgAndArgs ...interface{}) {
if !reflect.DeepEqual(want, got) {
fail(t, fmt.Sprintf("not equal: want: %+v, got: %+v", want, got), msgAndArgs)
}
}

// NoError asserts that the error is nil.
func NoError(t TestingT, err error, msgAndArgs ...interface{}) {
if err != nil {
fail(t, fmt.Sprintf("unexpected error: %v", err), msgAndArgs)
}
}

func fail(t TestingT, message string, msgAndArgs []interface{}) {
t.Helper()
userMessage := msgAndArgsToString(msgAndArgs)
if userMessage != "" {
message += ": " + userMessage
}
t.Errorf(message)
}

func msgAndArgsToString(msgAndArgs []interface{}) string {
if len(msgAndArgs) == 0 {
return ""
}
if len(msgAndArgs) == 1 {
return fmt.Sprintf("%+v", msgAndArgs[0])
}
if format, ok := msgAndArgs[0].(string); ok {
return fmt.Sprintf(format, msgAndArgs[1:]...)
}
return fmt.Sprintf("%+v", msgAndArgs)
}
160 changes: 160 additions & 0 deletions internal/assert/assert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package assert

import (
"errors"
"testing"
)

type call struct {
name string
args []interface{}
}

// fakeT is a fake implementation of TestingT.
// It records calls to its methods.
// Its methods are not safe for concurrent use.
type fakeT struct {
calls []call
}

func (f *fakeT) Errorf(format string, args ...interface{}) {
f.calls = append(f.calls, call{
name: "Errorf",
args: append([]interface{}{format}, args...),
})
}

func (f *fakeT) Helper() {
f.calls = append(f.calls, call{name: "Helper"})
}

func TestEqual(t *testing.T) {
tests := []struct {
name string
giveWant interface{}
giveGot interface{}
giveMsgAndArgs []interface{}
want []call
}{
{
name: "equal",
giveWant: 1,
giveGot: 1,
want: nil,
},
{
name: "not equal shallow",
giveWant: 1,
giveGot: 2,
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"not equal: want: 1, got: 2"}},
},
},
{
name: "not equal deep",
giveWant: map[string]interface{}{"foo": struct{ bar string }{"baz"}},
giveGot: map[string]interface{}{"foo": struct{ bar string }{"foobar"}},
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"not equal: want: map[foo:{bar:baz}], got: map[foo:{bar:foobar}]"}},
},
},
{
name: "with message",
giveWant: 1,
giveGot: 2,
giveMsgAndArgs: []interface{}{"user message"},
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"not equal: want: 1, got: 2: user message"}},
},
},
{
name: "with message and args",
giveWant: 1,
giveGot: 2,
giveMsgAndArgs: []interface{}{"user message: %d %s", 1, "arg2"},
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"not equal: want: 1, got: 2: user message: 1 arg2"}},
},
},
{
name: "only args",
giveWant: 1,
giveGot: 2,
giveMsgAndArgs: []interface{}{1, "arg2"},
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"not equal: want: 1, got: 2: [1 arg2]"}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var f fakeT
Equal(&f, tt.giveWant, tt.giveGot, tt.giveMsgAndArgs...)
// Since we're asserting ourselves it might be possible to introduce a subtle bug.
// However, the code is straightforward so it's not a big deal.
Equal(t, tt.want, f.calls)
})
}
}

func TestNoError(t *testing.T) {
tests := []struct {
name string
giveErr error
giveMsgAndArgs []interface{}
want []call
}{
{
name: "no error",
giveErr: nil,
want: nil,
},
{
name: "with error",
giveErr: errors.New("foo"),
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"unexpected error: foo"}},
},
},
{
name: "with message",
giveErr: errors.New("foo"),
giveMsgAndArgs: []interface{}{"user message"},
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"unexpected error: foo: user message"}},
},
},
{
name: "with message and args",
giveErr: errors.New("foo"),
giveMsgAndArgs: []interface{}{"user message: %d %s", 1, "arg2"},
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"unexpected error: foo: user message: 1 arg2"}},
},
},
{
name: "only args",
giveErr: errors.New("foo"),
giveMsgAndArgs: []interface{}{1, "arg2"},
want: []call{
{name: "Helper"},
{name: "Errorf", args: []interface{}{"unexpected error: foo: [1 arg2]"}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var f fakeT
NoError(&f, tt.giveErr, tt.giveMsgAndArgs...)
Equal(t, tt.want, f.calls)
})
}
}

0 comments on commit 6d30b8b

Please sign in to comment.