diff --git a/internal/assert/assert.go b/internal/assert/assert.go new file mode 100644 index 00000000..ae40e6af --- /dev/null +++ b/internal/assert/assert.go @@ -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) +} diff --git a/internal/assert/assert_test.go b/internal/assert/assert_test.go new file mode 100644 index 00000000..76a1c26d --- /dev/null +++ b/internal/assert/assert_test.go @@ -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) + }) + } +}