-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added an assertion package for tests
- Loading branch information
Showing
3 changed files
with
348 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
package mapstest | ||
|
||
import ( | ||
"fmt" | ||
maps "github.com/ansel1/vespucci/v4" | ||
"github.com/davecgh/go-spew/spew" | ||
"github.com/pmezard/go-difflib/difflib" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type strictMarker int | ||
|
||
// Strict is an option that can be passed to the Contains and Equivalent assertions. It | ||
// disables the default ContainsOptions. | ||
const Strict strictMarker = 0 | ||
|
||
// AssertContains returns true if maps.Contains(v1, v2). The following | ||
// ContainsOptions are automatically applied: | ||
// | ||
// - maps.EmptyMapValuesMatchAny | ||
// - maps.IgnoreTimeZones(true) | ||
// - maps.ParseTimes | ||
// | ||
// These default options can be suppressed by passing Strict in the options: | ||
// | ||
// AssertContains(t, v1, v2, Strict) | ||
// | ||
// optsMsgAndArgs can contain a string msg and a series of args, which | ||
// will be formatted into the assertion failure message. | ||
// | ||
// optsMsgAndArgs may also contain additional ContainOptions, which will be extracted | ||
// and applied to the Contains() function. | ||
func AssertContains(t TestingT, v1, v2 interface{}, optsMsgAndArgs ...interface{}) bool { | ||
if h, ok := t.(tHelper); ok { | ||
h.Helper() | ||
} | ||
opts, optsMsgAndArgs := splitOptions(optsMsgAndArgs) | ||
match := maps.ContainsMatch(v1, v2, opts...) | ||
|
||
if !assert.NoError(t, match.V1NormalizeError, "error normalizing v1") || !assert.NoError(t, match.V2NormalizeError, "error normalizing v2") { | ||
return false | ||
} | ||
|
||
if !match.Matches { | ||
return assert.Fail(t, fmt.Sprintf("v1 does not contain v2: \n"+ | ||
"%s%s", match.Message, containsDiff(match.V2, match.V2)), optsMsgAndArgs...) | ||
} | ||
|
||
return true | ||
} | ||
|
||
// AssertNotContains is the inverse of AssertContains | ||
func AssertNotContains(t TestingT, v1, v2 interface{}, optsMsgAndArgs ...interface{}) bool { | ||
if h, ok := t.(tHelper); ok { | ||
h.Helper() | ||
} | ||
opts, optsMsgAndArgs := splitOptions(optsMsgAndArgs) | ||
match := maps.ContainsMatch(v1, v2, opts...) | ||
|
||
if !assert.NoError(t, match.V1NormalizeError, "error normalizing v1") || !assert.NoError(t, match.V2NormalizeError, "error normalizing v2") { | ||
return false | ||
} | ||
|
||
if match.Matches { | ||
return assert.Fail(t, fmt.Sprintf("v1 should not contain v2: \n"+ | ||
"v1: %+v\n"+ | ||
"v2: %+v", match.V1, match.V2), optsMsgAndArgs...) | ||
} | ||
|
||
return true | ||
} | ||
|
||
// AssertEquivalent returns true if maps.Equivalent(v1, v2). The following | ||
// ContainsOptions are automatically applied: | ||
// | ||
// - maps.EmptyMapValuesMatchAny | ||
// - maps.IgnoreTimeZones(true) | ||
// - maps.ParseTimes | ||
// | ||
// optsMsgAndArgs can contain a string msg and a series of args, which | ||
// will be formatted into the assertion failure message. | ||
// | ||
// optsMsgAndArgs may also contain additional ContainOptions, which will be extracted | ||
// and applied to the Equivalent() function. | ||
func AssertEquivalent(t TestingT, v1, v2 interface{}, optsMsgAndArgs ...interface{}) bool { | ||
if h, ok := t.(tHelper); ok { | ||
h.Helper() | ||
} | ||
opts, optsMsgAndArgs := splitOptions(optsMsgAndArgs) | ||
match := maps.EquivalentMatch(v1, v2, opts...) | ||
|
||
if !assert.NoError(t, match.V1NormalizeError, "error normalizing v1") || !assert.NoError(t, match.V2NormalizeError, "error normalizing v2") { | ||
return false | ||
} | ||
|
||
if !match.Matches { | ||
return assert.Fail(t, fmt.Sprintf("v1 !≈ v2: \n"+ | ||
"%s%s", match.Message, containsDiff(match.V2, match.V2)), optsMsgAndArgs...) | ||
} | ||
|
||
return true | ||
} | ||
|
||
// AssertNotEquivalent is the inverse of AssertEquivalent | ||
func AssertNotEquivalent(t TestingT, v1, v2 interface{}, optsMsgAndArgs ...interface{}) bool { | ||
if h, ok := t.(tHelper); ok { | ||
h.Helper() | ||
} | ||
opts, optsMsgAndArgs := splitOptions(optsMsgAndArgs) | ||
match := maps.EquivalentMatch(v1, v2, opts...) | ||
|
||
if !assert.NoError(t, match.V1NormalizeError, "error normalizing v1") || !assert.NoError(t, match.V2NormalizeError, "error normalizing v2") { | ||
return false | ||
} | ||
|
||
if match.Matches { | ||
return assert.Fail(t, fmt.Sprintf("v1 should not ≈ v2: \n"+ | ||
"v1: %+v\n"+ | ||
"v2: %+v", match.V1, match.V2), optsMsgAndArgs...) | ||
} | ||
|
||
return true | ||
} | ||
|
||
// RequireContains is like AssertContains, but fails the test immediately. | ||
func RequireContains(t TestingT, v1, v2 interface{}, optsMsgAndArgs ...interface{}) { | ||
if h, ok := t.(tHelper); ok { | ||
h.Helper() | ||
} | ||
if !AssertContains(t, v1, v2, optsMsgAndArgs...) { | ||
t.FailNow() | ||
} | ||
} | ||
|
||
// RequireNotContains is like AssertNotContains, but fails the test immediately. | ||
func RequireNotContains(t TestingT, v1, v2 interface{}, optsMsgAndArgs ...interface{}) { | ||
if h, ok := t.(tHelper); ok { | ||
h.Helper() | ||
} | ||
if !AssertNotContains(t, v1, v2, optsMsgAndArgs...) { | ||
t.FailNow() | ||
} | ||
} | ||
|
||
// RequireEquivalent is like AssertEquivalent, but fails the test immediately. | ||
func RequireEquivalent(t TestingT, v1, v2 interface{}, optsMsgAndArgs ...interface{}) { | ||
if h, ok := t.(tHelper); ok { | ||
h.Helper() | ||
} | ||
if !AssertEquivalent(t, v1, v2, optsMsgAndArgs...) { | ||
t.FailNow() | ||
} | ||
} | ||
|
||
// RequireNotEquivalent is like AssertNotEquivalent, but fails the test immediately. | ||
func RequireNotEquivalent(t TestingT, v1, v2 interface{}, optsMsgAndArgs ...interface{}) { | ||
if h, ok := t.(tHelper); ok { | ||
h.Helper() | ||
} | ||
if !AssertNotEquivalent(t, v1, v2, optsMsgAndArgs...) { | ||
t.FailNow() | ||
} | ||
} | ||
|
||
// containsDiff returns a diff of both values as long as both are of the same type and | ||
// are a struct, map, slice or array. Otherwise it returns an empty string. | ||
func containsDiff(v1 interface{}, v2 interface{}) string { | ||
spewC := spew.ConfigState{ | ||
Indent: " ", | ||
DisablePointerAddresses: true, | ||
DisableCapacities: true, | ||
SortKeys: true, | ||
} | ||
e := spewC.Sdump(v1) | ||
a := spewC.Sdump(v2) | ||
|
||
diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ | ||
A: difflib.SplitLines(e), | ||
B: difflib.SplitLines(a), | ||
FromFile: "v1", | ||
FromDate: "", | ||
ToFile: "v2", | ||
ToDate: "", | ||
Context: 1, | ||
}) | ||
|
||
return "\n\nDiff:\n" + diff | ||
} | ||
|
||
// removes any instances of DeepContainsOption from args, and uses them to create | ||
// a deepContainsOptions. Returns the initialized options, which will never be nil, | ||
// and any remaining items in args. | ||
func splitOptions(args []interface{}) (opts []maps.ContainsOption, msgAndArgs []interface{}) { | ||
msgAndArgs = args[:0] | ||
var strict bool | ||
|
||
for _, arg := range args { | ||
switch t := arg.(type) { | ||
case strictMarker: | ||
strict = true | ||
case maps.ContainsOption: | ||
opts = append(opts, t) | ||
default: | ||
msgAndArgs = append(msgAndArgs, arg) | ||
} | ||
} | ||
|
||
if !strict { | ||
opts = append(opts, | ||
maps.EmptyMapValuesMatchAny(), | ||
maps.IgnoreTimeZones(true), | ||
maps.ParseTimes(), | ||
) | ||
} | ||
|
||
return | ||
} | ||
|
||
type tHelper interface { | ||
Helper() | ||
} | ||
|
||
// TestingT is a subset of testing.TB | ||
type TestingT = require.TestingT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package mapstest | ||
|
||
import ( | ||
maps "github.com/ansel1/vespucci/v4" | ||
"github.com/stretchr/testify/assert" | ||
"testing" | ||
) | ||
|
||
type mockTestingT struct { | ||
failed bool | ||
failedNow bool | ||
} | ||
|
||
func (m *mockTestingT) Logf(_ string, _ ...interface{}) { | ||
|
||
} | ||
|
||
func (m *mockTestingT) Errorf(_ string, _ ...interface{}) { | ||
m.failed = true | ||
} | ||
|
||
func (m *mockTestingT) FailNow() { | ||
m.failedNow = true | ||
} | ||
|
||
type dict = map[string]interface{} | ||
|
||
func TestAssertionsContains(t *testing.T) { | ||
|
||
tests := []struct { | ||
v1, v2 interface{} | ||
contains bool | ||
equiv bool | ||
opts []interface{} | ||
}{ | ||
{ | ||
v1: "red", | ||
v2: "red", | ||
contains: true, | ||
equiv: true, | ||
}, | ||
{ | ||
v1: "red", | ||
v2: "blue", | ||
contains: false, | ||
equiv: false, | ||
}, | ||
{ | ||
v1: "red", | ||
v2: "", | ||
contains: true, | ||
equiv: true, | ||
}, | ||
{ | ||
v1: "red", | ||
v2: "blue", | ||
contains: false, | ||
equiv: false, | ||
opts: []interface{}{Strict}, | ||
}, | ||
{ | ||
v1: "redblue", | ||
v2: "blue", | ||
contains: true, | ||
equiv: true, | ||
opts: []interface{}{maps.StringContains()}, | ||
}, | ||
{ | ||
v1: dict{"color": "red", "size": 1}, | ||
v2: dict{"color": "red"}, | ||
contains: true, | ||
equiv: false, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run("", func(t *testing.T) { | ||
t.Logf("v1: %+v", test.v1) | ||
t.Logf("v2: %+v", test.v2) | ||
|
||
type assertFunc func(TestingT, interface{}, interface{}, ...interface{}) bool | ||
type requireFunc func(TestingT, interface{}, interface{}, ...interface{}) | ||
|
||
af := func(fn assertFunc, expectSuccess bool) { | ||
mt := mockTestingT{} | ||
b := fn(&mt, test.v1, test.v2, test.opts...) | ||
assert.Equal(t, expectSuccess, b) | ||
if expectSuccess { | ||
assert.False(t, mt.failed) | ||
assert.False(t, mt.failedNow) | ||
} else { | ||
assert.True(t, mt.failed) | ||
assert.False(t, mt.failedNow) | ||
} | ||
} | ||
|
||
rf := func(fn requireFunc, expectSuccess bool) { | ||
mt := mockTestingT{} | ||
fn(&mt, test.v1, test.v2, test.opts...) | ||
if expectSuccess { | ||
assert.False(t, mt.failed) | ||
assert.False(t, mt.failedNow) | ||
} else { | ||
assert.True(t, mt.failed) | ||
assert.True(t, mt.failedNow) | ||
} | ||
} | ||
|
||
af(AssertContains, test.contains) | ||
af(AssertNotContains, !test.contains) | ||
rf(RequireContains, test.contains) | ||
rf(RequireNotContains, !test.contains) | ||
|
||
af(AssertEquivalent, test.equiv) | ||
af(AssertNotEquivalent, !test.equiv) | ||
rf(RequireEquivalent, test.equiv) | ||
rf(RequireNotEquivalent, !test.equiv) | ||
|
||
}) | ||
} | ||
} |