Skip to content

Commit

Permalink
Added an assertion package for tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ansel1 committed Jan 28, 2021
1 parent 70b7ff6 commit 5df763c
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 2 deletions.
4 changes: 2 additions & 2 deletions maps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ v2.time -> 1987-02-10T05:30:15-06:00`,
})
}

func TestContainsEx(t *testing.T) {
func TestContainsMatch(t *testing.T) {
w1 := Widget{
Size: 1,
Color: "red",
Expand Down Expand Up @@ -603,7 +603,7 @@ v2 -> map[color:big size:1]`, trace)
assert.True(t, Equivalent(v1, v2, EmptyValuesMatchAny()))
}

func TestEquivalentEx(t *testing.T) {
func TestEquivalentMatch(t *testing.T) {
w1 := Widget{
Size: 1,
Color: "red",
Expand Down
225 changes: 225 additions & 0 deletions mapstest/assertions.go
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
121 changes: 121 additions & 0 deletions mapstest/assertions_test.go
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)

})
}
}

0 comments on commit 5df763c

Please sign in to comment.