From 5df763c8ef73f03d84f97a74a7f28e36c8315369 Mon Sep 17 00:00:00 2001 From: Russ Egan Date: Wed, 27 Jan 2021 21:38:14 -0600 Subject: [PATCH] Added an assertion package for tests --- maps_test.go | 4 +- mapstest/assertions.go | 225 ++++++++++++++++++++++++++++++++++++ mapstest/assertions_test.go | 121 +++++++++++++++++++ 3 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 mapstest/assertions.go create mode 100644 mapstest/assertions_test.go diff --git a/maps_test.go b/maps_test.go index a208ede..af7adfc 100644 --- a/maps_test.go +++ b/maps_test.go @@ -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", @@ -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", diff --git a/mapstest/assertions.go b/mapstest/assertions.go new file mode 100644 index 0000000..2ed89d8 --- /dev/null +++ b/mapstest/assertions.go @@ -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 diff --git a/mapstest/assertions_test.go b/mapstest/assertions_test.go new file mode 100644 index 0000000..1a2ce8f --- /dev/null +++ b/mapstest/assertions_test.go @@ -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) + + }) + } +}