From c340bb4300ed78b91e257a204d52b7d88871ae58 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 1 Apr 2023 14:30:35 +0100 Subject: [PATCH] Generics (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v.1.1.0 - generics Changes all the matchers to use generics instead of reflection. Some still use a bit of reflection, e.g. TypeName etc. Other major changes: ValueContaining has been split into StringContaining, MapContaining, MapContainingValues, MapMatchingValues, ArrayContaining and ArrayMatching. No longer panics with unknown types, as types will fail at compile time. Some idiosyncrasies with the generic types do exist, but this is language specific; map matchers generally need to know the type of the map key values explicitly or the compiler will complain, e.g. then.AssertThat(testing, map[string]bool{"hi": true, "bye": true}, has.AllKeys[string, bool]("hi", "bye")) has.Length() is likewise pernickety about types being explicit, mainly because it works on both strings and arrays. It needs to know both the type of the array and the array/string type. Confused? me too. is.LessThan and is.GreaterThan no longer work on complex types. This is because the complex types do not support the comparison operators (yet, somehow, they could be compared by reflection 🤷 ) See the matcher_test.go file for full usage. --------- Co-authored-by: mattcorby-eaglen --- .travis.yml | 15 - README.md | 28 +- by/eventually.go | 19 +- go.mod | 8 +- has/haseveryelement.go | 38 +- has/hasfield.go | 6 +- has/hasfunction.go | 6 +- has/haskey.go | 38 +- has/haslength.go | 65 ++- has/hasprefix.go | 9 +- has/hasstructvalues.go | 26 +- has/hassuffix.go | 9 +- has/hastypename.go | 41 +- is/allof.go | 10 +- is/anyof.go | 8 +- is/contains.go | 213 ++++++---- is/describe.go | 2 +- is/empty.go | 20 +- is/equalto.go | 19 +- is/equaltoignoringwhitespace.go | 9 +- is/greaterthan.go | 29 +- is/greaterthanorequalto.go | 8 +- is/isfalse.go | 8 +- is/istrue.go | 6 +- is/lessthan.go | 23 +- is/lessthanorequalto.go | 6 +- is/matchespattern.go | 8 +- is/nil.go | 8 +- is/not.go | 6 +- matcher.go | 12 +- matcher_test.go | 731 ++++++++++++++++++++------------ then/assertthat.go | 4 +- then/eventually.go | 44 +- 33 files changed, 870 insertions(+), 612 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 737aed6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -sudo: false -language: go -go: - - 1.9.x - - master -before_install: - - go get github.com/mattn/goveralls -script: - - $GOPATH/bin/goveralls -service=travis-ci -matrix: - allow_failures: - - go: master - fast_finish: true -notifications: - email: false diff --git a/README.md b/README.md index a820e4b..e1d29a3 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Expected: value equal to Composed with AllOf: ```go -then.AssertThat(t, "abcdef", is.AllOf(is.ValueContaining("abc"), is.LessThan("ghi"))) +then.AssertThat(t, "abcdef", is.AllOf(is.StringContaining("abc"), is.LessThan("ghi"))) ``` Asynchronous Matching (v1.0.8 onwards): @@ -60,12 +60,36 @@ then.WithinTenSeconds(t, func(eventually gocrest.TestingT) { then.AssertThat(eventually, by.Channelling(channelTwo), is.EqualTo("11").Reason("This is unreachable")) }) ``` +# v.1.1.0 - generics + +Changes all the matchers to use generics instead of reflection. Some still use a bit of reflection, e.g. TypeName etc. + +## Other major changes: + +* ValueContaining has been split into StringContaining, MapContaining, MapContainingValues, MapMatchingValues, ArrayContaining and ArrayMatching. +* No longer panics with unknown types, as types will fail at compile time. +Some idiosyncrasies with the generic types do exist, but this is language specific; + +* Map matchers generally need to know the type of the map key values explicitly or the compiler will complain, e.g. +``` +then.AssertThat(testing, map[string]bool{"hi": true, "bye": true}, has.AllKeys[string, bool]("hi", "bye")) +``` +* `has.Length()` is likewise pernickety about types being explicit, mainly because it works on both strings and arrays. It needs to know both the type of the array and the array/string type. Confused? me too. +* `is.LessThan()` and `is.GreaterThan()` (and by extension `is.GreaterThanOrEqualTo` and `is.LessThanOrEqualTo`) no longer work on complex types. This is because the complex types do not support the comparison operators (yet, somehow, they could be compared by reflection 🤷 ) + +See the matcher_test.go file for full usage. + # Matchers so far.. - is.EqualTo(x) - is.EqualToIgnoringWhitespace(string) - compares two strings without comparing their whitespace characters. - is.Nil() - value must be nil -- is.ValueContaining(expected) -- acts like containsAll +- is.StringContaining(expected) -- acts like containsAll +- is.MapContaining(expected) -- acts like containsAll +- is.MapContainingValues(expected) -- acts like containsAll +- is.MapMatchingValues(expected) -- acts like containsAll +- is.ArrayContaining(expected) -- acts like containsAll +- is.ArrayMatching(expected) -- acts like containsAll - is.Not(m *Matcher) -- logical not of matcher's result - is.MatchForPattern(regex string) -- a string regex expression - has.FunctionNamed(string x) - checks if an interface has a function (method) diff --git a/by/eventually.go b/by/eventually.go index b1c3243..197c1fc 100644 --- a/by/eventually.go +++ b/by/eventually.go @@ -3,18 +3,21 @@ package by import ( "bufio" "io" - "reflect" ) -func Channelling(actual interface{}) interface{} { - var selectCase = make([]reflect.SelectCase, 1) - selectCase[0].Dir = reflect.SelectRecv - selectCase[0].Chan = reflect.ValueOf(actual) - _, recv, _ := reflect.Select(selectCase) - return recv.Interface() +// Channelling channels any channel of type T and returns the value from the channel +func Channelling[T any](actual chan T) T { + return <-actual } -func Reading(actual io.Reader, len int) interface{} { + +// Reading peeks at the value of a reader by reading `len` bytes ahead. +func Reading(actual io.Reader, len int) []byte { reader := bufio.NewReader(actual.(io.Reader)) peek, _ := reader.Peek(len) return peek } + +// Calling calls the function passed and returns the value +func Calling[K any, T any](actual func(T) K, value T) K { + return actual(value) +} diff --git a/go.mod b/go.mod index cc84e1a..7ab3a38 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module github.com/corbym/gocrest -go 1.15 +go 1.18 + +require ( + golang.org/x/sys v0.4.0 // indirect + golang.org/x/tools v0.5.1-0.20230111220935-a7f7db3f17fc // indirect + golang.org/x/tools/cmd/cover v0.1.0-deprecated // indirect +) diff --git a/has/haseveryelement.go b/has/haseveryelement.go index 8c7e31b..967a4e1 100644 --- a/has/haseveryelement.go +++ b/has/haseveryelement.go @@ -2,51 +2,37 @@ package has import ( "fmt" - "reflect" - "github.com/corbym/gocrest" ) // EveryElement Checks whether the nth element of the array/slice matches the nth expectation passed -// Panics if the actual is not an array/slice -// Panics if the count of the expectations does not match the array's/slice's length -func EveryElement(expects ...*gocrest.Matcher) *gocrest.Matcher { - match := new(gocrest.Matcher) +func EveryElement[A any](expects ...*gocrest.Matcher[A]) *gocrest.Matcher[[]A] { + match := new(gocrest.Matcher[[]A]) match.Describe = fmt.Sprintf("elements to match %s", describe(expects, "and")) for _, e := range expects { match.AppendActual(e.Actual) } - match.Matches = func(actual interface{}) bool { - - actualValue := reflect.ValueOf(actual) - switch actualValue.Kind() { - case reflect.Array, reflect.Slice: + match.Matches = func(actual []A) bool { + if len(actual) != len(expects) { + return false + } - if actualValue.Len() != len(expects) { + for i := 0; i < len(actual); i++ { + result := expects[i].Matches(actual[i]) + if !result { return false } - - for i := 0; i < actualValue.Len(); i++ { - result := expects[i].Matches(actualValue.Index(i).Interface()) - - if !result { - return false - } - } - - return true - - default: - panic("cannot determine type of variadic actual, " + actualValue.String()) } + + return true } return match } -func describe(matchers []*gocrest.Matcher, conjunction string) string { +func describe[A any](matchers []*gocrest.Matcher[A], conjunction string) string { var description string for x := 0; x < len(matchers); x++ { description += fmt.Sprintf("[%v]:%v", x, matchers[x].Describe) diff --git a/has/hasfield.go b/has/hasfield.go index a8e8ca7..cf4943f 100644 --- a/has/hasfield.go +++ b/has/hasfield.go @@ -8,10 +8,10 @@ import ( // FieldNamed is a naive implementation for testing if a struct has a particular field name. Does not check type. // Returns a matcher that will use reflect to check if the actual has the method given by expected -func FieldNamed(expected string) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func FieldNamed[A any](expected string) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) matcher.Describe = fmt.Sprintf("struct with function %s", expected) - matcher.Matches = func(actual interface{}) bool { + matcher.Matches = func(actual A) bool { typeOfActual := reflect.TypeOf(actual) matcher.Actual = fieldStringValue(typeOfActual) expectedName := reflect.ValueOf(expected).String() diff --git a/has/hasfunction.go b/has/hasfunction.go index df0e50d..6ec751e 100644 --- a/has/hasfunction.go +++ b/has/hasfunction.go @@ -8,10 +8,10 @@ import ( // FunctionNamed implementation for testing if a Type has a particular method name. Does not check parameters. // Returns a matcher that will use reflect to check if the actual has the method given by expected. -func FunctionNamed(expected string) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func FunctionNamed[A any](expected string) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) matcher.Describe = fmt.Sprintf("interface with function %s", expected) - matcher.Matches = func(actual interface{}) bool { + matcher.Matches = func(actual A) bool { typeOfActual := reflect.TypeOf(actual) matcher.Actual = actualStringValue(typeOfActual) expectedName := reflect.ValueOf(expected).String() diff --git a/has/haskey.go b/has/haskey.go index 21b7e88..27c9cef 100644 --- a/has/haskey.go +++ b/has/haskey.go @@ -3,31 +3,27 @@ package has import ( "fmt" "github.com/corbym/gocrest" - "reflect" ) // Key is a matcher that checks if actual has a key == expected. -// Panics when actual's Kind is not a map. // Returns a matcher that matches when a map has key == expected -func Key(expected interface{}) *gocrest.Matcher { - matcher := new(gocrest.Matcher) - matcher.Describe = fmt.Sprintf("map has key '%s'", expected) - matcher.Matches = func(actual interface{}) bool { +func Key[K comparable, V any](expected K) *gocrest.Matcher[map[K]V] { + matcher := new(gocrest.Matcher[map[K]V]) + matcher.Describe = fmt.Sprintf("map has key '%v'", expected) + matcher.Matches = func(actual map[K]V) bool { return hasKey(actual, expected) } return matcher } // AllKeys is a matcher that checks if map actual has all keys == expecteds. -// Panics when actual's Kind is not a map. // Returns a matcher that matches when a map has all keys == all expected. -func AllKeys(expected ...interface{}) *gocrest.Matcher { - matcher := new(gocrest.Matcher) - matcher.Describe = fmt.Sprintf("map has keys '%s'", expected) - matcher.Matches = func(actual interface{}) bool { - keyValuesToMatch := reflect.ValueOf(correctExpectedValue(expected...)) - for x := 0; x < keyValuesToMatch.Len(); x++ { - if !hasKey(actual, keyValuesToMatch.Index(x).Interface()) { +func AllKeys[K comparable, V any](expected ...K) *gocrest.Matcher[map[K]V] { + matcher := new(gocrest.Matcher[map[K]V]) + matcher.Describe = fmt.Sprintf("map has keys '%v'", expected) + matcher.Matches = func(actual map[K]V) bool { + for _, k := range expected { + if !hasKey(actual, k) { return false } } @@ -35,18 +31,10 @@ func AllKeys(expected ...interface{}) *gocrest.Matcher { } return matcher } -func correctExpectedValue(expected ...interface{}) interface{} { - kind := reflect.ValueOf(expected[0]).Kind() - if kind == reflect.Slice { - return expected[0] - } - return expected -} -func hasKey(actual interface{}, expected interface{}) bool { - mapKeys := reflect.ValueOf(actual).MapKeys() - for x := 0; x < len(mapKeys); x++ { - if mapKeys[x].Interface() == expected { +func hasKey[K comparable, V any](actual map[K]V, expected K) bool { + for k := range actual { + if k == expected { return true } } diff --git a/has/haslength.go b/has/haslength.go index 976f9c8..d5fd30a 100644 --- a/has/haslength.go +++ b/has/haslength.go @@ -3,29 +3,62 @@ package has import ( "fmt" "github.com/corbym/gocrest" - "reflect" ) -// Length can be called with arrays, maps, *gocrest.Matcher and strings but not numeric types. -// has.Length(is.GreaterThan(x)) is a valid call. +// Length can be called with arrays and strings // Returns a matcher that matches if the length matches the given criteria -func Length(expected interface{}) *gocrest.Matcher { +func Length[V any, A []V | string](expected int) *gocrest.Matcher[A] { const description = "value with length %v" - matcher := new(gocrest.Matcher) + matcher := new(gocrest.Matcher[A]) matcher.Describe = fmt.Sprintf(description, expected) - matcher.Matches = func(actual interface{}) bool { - if actual == nil { - return false - } - lenOfActual := reflect.ValueOf(actual).Len() + matcher.Matches = func(actual A) bool { + lenOfActual := len(actual) matcher.Actual = fmt.Sprintf("length was %d", lenOfActual) - switch expected.(type) { - case *gocrest.Matcher: - m := expected.(*gocrest.Matcher) - matcher.Describe = fmt.Sprintf(description, m.Describe) - return m.Matches(lenOfActual) - } return lenOfActual == expected } return matcher } + +// MapLength can be called with maps +// Returns a matcher that matches if the length matches the given criteria +func MapLength[K comparable, V any](expected int) *gocrest.Matcher[map[K]V] { + const description = "value with length %v" + matcher := new(gocrest.Matcher[map[K]V]) + matcher.Describe = fmt.Sprintf(description, expected) + matcher.Matches = func(actual map[K]V) bool { + lenOfActual := len(actual) + matcher.Actual = fmt.Sprintf("length was %d", lenOfActual) + return lenOfActual == expected + } + return matcher +} + +// LengthMatching can be called with arrays or strings +// Returns a matcher that matches if the length matches matcher passed in +func LengthMatching[V any, A []V | string](expected *gocrest.Matcher[int]) *gocrest.Matcher[A] { + const description = "value with length %v" + matcher := new(gocrest.Matcher[A]) + matcher.Describe = fmt.Sprintf(description, expected) + matcher.Matches = func(actual A) bool { + lenOfActual := len(actual) + matcher.Actual = fmt.Sprintf("length was %d", lenOfActual) + return expected.Matches(lenOfActual) + + } + return matcher +} + +// MapLengthMatching can be called with maps +// Returns a matcher that matches if the length matches the given matcher +func MapLengthMatching[K comparable, V any](expected *gocrest.Matcher[int]) *gocrest.Matcher[map[K]V] { + const description = "value with length %v" + matcher := new(gocrest.Matcher[map[K]V]) + matcher.Describe = fmt.Sprintf(description, expected) + matcher.Matches = func(actual map[K]V) bool { + lenOfActual := len(actual) + matcher.Actual = fmt.Sprintf("length was %d", lenOfActual) + return expected.Matches(lenOfActual) + + } + return matcher +} diff --git a/has/hasprefix.go b/has/hasprefix.go index 4dcf10b..77a21a1 100644 --- a/has/hasprefix.go +++ b/has/hasprefix.go @@ -7,14 +7,13 @@ import ( ) // Prefix returns a matcher that matches if the given string is prefixed with the expected string -// Function panics if the actual is not a string. // Uses strings.Prefix(act, exp) to evaluate strings. // Returns a matcher that returns true if the above conditions are met -func Prefix(expected string) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func Prefix(expected string) *gocrest.Matcher[string] { + matcher := new(gocrest.Matcher[string]) matcher.Describe = fmt.Sprintf("value with prefix %s", expected) - matcher.Matches = func(actual interface{}) bool { - return strings.HasPrefix(actual.(string), expected) + matcher.Matches = func(actual string) bool { + return strings.HasPrefix(actual, expected) } return matcher } diff --git a/has/hasstructvalues.go b/has/hasstructvalues.go index b544791..273851d 100644 --- a/has/hasstructvalues.go +++ b/has/hasstructvalues.go @@ -2,13 +2,12 @@ package has import ( "fmt" - "reflect" - "github.com/corbym/gocrest" + "reflect" ) -// StructMatchers Type that can be passed to StructWithValues. Mapp Struct field names to a matcher -type StructMatchers map[string]*gocrest.Matcher +// StructMatchers Type that can be passed to StructWithValues. Map Struct field names to a matcher +type StructMatchers[A any] map[string]*gocrest.Matcher[A] // StructWithValues Checks whether the actual struct matches all expectations passed as StructMatchers. // This method can be used to check single struct fields in different ways or omit checking some struct fields at all. @@ -16,15 +15,15 @@ type StructMatchers map[string]*gocrest.Matcher // Panics if the actual value is not a struct. // Panics if Structmatchers contains a key that can not be found in the actual struct. // Panics if Structmatchers contains a key that is unexported. -func StructWithValues(expects StructMatchers) *gocrest.Matcher { - match := new(gocrest.Matcher) +func StructWithValues[A any, B any](expects StructMatchers[B]) *gocrest.Matcher[A] { + match := new(gocrest.Matcher[A]) match.Describe = fmt.Sprintf("struct values to match {%s}", describeStructMatchers(expects)) for _, e := range expects { match.AppendActual(e.Actual) } - match.Matches = func(actual interface{}) bool { + match.Matches = func(actual A) bool { actualValue := reflect.ValueOf(actual) if actualValue.Kind() == reflect.Ptr { @@ -38,22 +37,23 @@ func StructWithValues(expects StructMatchers) *gocrest.Matcher { if !v.IsValid() { panic(fmt.Sprintf("Expect[%v] does not exist on actual struct", key)) } - - result := expect.Matches(v.Interface()) - - if !result { + var matches = false + if value, ok := v.Interface().(B); ok { + matches = expect.Matches(value) + } + if !matches { return false } } return true } - panic("cannot determine type of variadic actual, " + actualValue.String()) + panic("cannot determine type of actual, " + actualValue.String()) } return match } -func describeStructMatchers(matchers StructMatchers) string { +func describeStructMatchers[A any](matchers StructMatchers[A]) string { description := "" bindCount := 0 diff --git a/has/hassuffix.go b/has/hassuffix.go index c4bb926..6578e77 100644 --- a/has/hassuffix.go +++ b/has/hassuffix.go @@ -7,14 +7,13 @@ import ( ) // Suffix returns a matcher that matches if the given string is suffixed with the expected string. -// Panics if the actual is not a string. // Uses strings.HasSuffix(act,exp) to evaluate strings. // Returns a matcher that returns true if the above conditions are met. -func Suffix(expected string) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func Suffix(expected string) *gocrest.Matcher[string] { + matcher := new(gocrest.Matcher[string]) matcher.Describe = fmt.Sprintf("value with suffix %s", expected) - matcher.Matches = func(actual interface{}) bool { - return strings.HasSuffix(actual.(string), expected) + matcher.Matches = func(actual string) bool { + return strings.HasSuffix(actual, expected) } return matcher } diff --git a/has/hastypename.go b/has/hastypename.go index e13ce81..ff16773 100644 --- a/has/hastypename.go +++ b/has/hastypename.go @@ -5,25 +5,34 @@ import ( "reflect" ) -// TypeName returns true if the expected matches the actual Type's Name. Expected can be a matcher or a string. -// E.g. has.TypeName(EqualTo("pkg.Type")) would be true with instance of `type Type struct{}` in package name 'pkg'. -func TypeName(expected interface{}) *gocrest.Matcher { - matcher := new(gocrest.Matcher) - matcher.Matches = func(actual interface{}) bool { +// TypeName returns true if the expected matches the actual Type's Name. +// E.g. has.TypeName("pkg.Type") would be true with instance of `type Type struct{}` in package name 'pkg'. +func TypeName[A any](expected string) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) + matcher.Matches = func(actual A) bool { actualTypeName := reflect.TypeOf(actual).String() matcher.Actual = actualTypeName matcher.Describe = "has type " - switch expected.(type) { - case *gocrest.Matcher: - m := expected.(*gocrest.Matcher) - matches := m.Matches(actualTypeName) - matcher.AppendActual(m.Actual) - matcher.Describe += m.Describe - return matches - default: - matcher.Describe += "<" + expected.(string) + ">" - return actualTypeName == expected - } + matcher.Describe += "<" + expected + ">" + return actualTypeName == expected + + } + return matcher +} + +// TypeNameMatches returns true if the expected matches the actual Type's Name using the given matcher. +// E.g. has.TypeName(is.EqualTo("pkg.Type")) would be true with instance of `type Type struct{}` in package name 'pkg'. +func TypeNameMatches[A any](expected *gocrest.Matcher[string]) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) + matcher.Matches = func(actual A) bool { + actualTypeName := reflect.TypeOf(actual).String() + matcher.Actual = actualTypeName + matcher.Describe = "has type " + + matches := expected.Matches(actualTypeName) + matcher.AppendActual(expected.Actual) + matcher.Describe += expected.Describe + return matches } return matcher } diff --git a/is/allof.go b/is/allof.go index 4e67243..a466652 100644 --- a/is/allof.go +++ b/is/allof.go @@ -7,18 +7,18 @@ import ( // AllOf takes some matchers and checks if all the matchers return true. // Returns a matcher that performs the test on the input matchers. -func AllOf(allMatchers ...*gocrest.Matcher) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func AllOf[A any](allMatchers ...*gocrest.Matcher[A]) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) matcher.Describe = fmt.Sprintf("all of (%s)", describe("and", allMatchers)) matcher.Matches = matchAll(allMatchers, matcher) return matcher } -func matchAll(allMatchers []*gocrest.Matcher, allOf *gocrest.Matcher) func(actual interface{}) bool { - return func(actual interface{}) bool { +func matchAll[A any](allMatchers []*gocrest.Matcher[A], allOf *gocrest.Matcher[A]) func(actual A) bool { + return func(actual A) bool { allOf.AppendActual(fmt.Sprintf("actual <%v>", actual)) matches := true - var failingMatchers []*gocrest.Matcher + var failingMatchers []*gocrest.Matcher[A] for x := 0; x < len(allMatchers); x++ { if !allMatchers[x].Matches(actual) { matches = false diff --git a/is/anyof.go b/is/anyof.go index 770a3ad..d9b8d2a 100644 --- a/is/anyof.go +++ b/is/anyof.go @@ -7,15 +7,15 @@ import ( // AnyOf takes some matchers and checks if at least one of the matchers return true. // Returns a matcher that performs the test on the input matchers. -func AnyOf(allMatchers ...*gocrest.Matcher) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func AnyOf[A any](allMatchers ...*gocrest.Matcher[A]) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) matcher.Describe = fmt.Sprintf("any of (%s)", describe("or", allMatchers)) matcher.Matches = anyMatcherMatches(allMatchers, matcher) return matcher } -func anyMatcherMatches(allMatchers []*gocrest.Matcher, anyOf *gocrest.Matcher) func(actual interface{}) bool { - return func(actual interface{}) bool { +func anyMatcherMatches[A any](allMatchers []*gocrest.Matcher[A], anyOf *gocrest.Matcher[A]) func(actual A) bool { + return func(actual A) bool { matches := false anyOf.AppendActual(fmt.Sprintf("actual <%v>", actual)) for x := 0; x < len(allMatchers); x++ { diff --git a/is/contains.go b/is/contains.go index e8e26fe..0557178 100644 --- a/is/contains.go +++ b/is/contains.go @@ -3,124 +3,163 @@ package is import ( "fmt" "github.com/corbym/gocrest" - "reflect" "strings" ) -// ValueContaining finds if x is contained in y. +// StringContaining finds if all x's are contained as value in y. // Acts like "ContainsAll", all elements given must be present (or must match) in actual in the same order as the expected values. -// If "expected" is an array or slice, we assume that actual is the same type. -// assertThat([]T, has.ValueContaining(a,b,c)) is also valid if variadic a,b,c are all type T (or matchers of T). -// For maps, the expected must also be a map or a variadic of expected values (or value matchers) and matches when -// both maps contain all key,values in expected or all variadic values are equal (or matchers match) respectively. -// For string, behaves like strings.Contains. -// Will panic if types cannot be converted correctly. -// Returns the Matcher that returns true if found. -func ValueContaining(expected ...interface{}) *gocrest.Matcher { - match := new(gocrest.Matcher) - correctVariadicExpected := correctExpectedValue(expected...) - match.Describe = fmt.Sprintf("something that contains %v", descriptionFor(expected...)) - match.Matches = func(actual interface{}) bool { - expectedAsStr, expectedOk := expected[0].(string) - actualAsStr, actualOk := actual.(string) - if expectedOk && actualOk { - return strings.Contains(actualAsStr, expectedAsStr) - } - actualValue := reflect.ValueOf(actual) - expectedValue := reflect.ValueOf(correctVariadicExpected) - switch actualValue.Kind() { - case reflect.Array, reflect.Slice: - return listContains(expectedValue, actualValue) - case reflect.Map: - if expectedValue.Kind() == reflect.Array || expectedValue.Kind() == reflect.Slice { - return mapContainsList(expectedValue, actualValue) +func StringContaining(expected ...string) *gocrest.Matcher[string] { + match := new(gocrest.Matcher[string]) + match.Describe = fmt.Sprintf("something that contains %v", expected) + match.Matches = func(actual string) bool { + for i := 0; i < len(expected); i++ { + if !strings.Contains(actual, expected[i]) { + return false } - return mapContains(expectedValue, actualValue) - default: - panic("cannot determine type of variadic actual, " + actualValue.String()) } + return true } return match } -func mapContainsList(expected reflect.Value, mapValue reflect.Value) bool { - contains := make(map[interface{}]bool) - for i := 0; i < expected.Len(); i++ { - for _, key := range mapValue.MapKeys() { - itemValue := expected.Index(i).Interface() - typeMatcher, ok := itemValue.(*gocrest.Matcher) - actualValue := mapValue.MapIndex(key).Interface() - if ok { - if typeMatcher.Matches(actualValue) { - contains[itemValue] = true - } - } else { - if actualValue == itemValue { - contains[itemValue] = true - } - } +// MapContaining finds if all map[k] 's value V is contained as a value of actual[k] +// Acts like "ContainsAll", all elements given must be present in actual in the same order as the expected values. +func MapContaining[K comparable, V comparable](expected map[K]V) *gocrest.Matcher[map[K]V] { + match := new(gocrest.Matcher[map[K]V]) + match.Describe = fmt.Sprintf("something that contains %v", expected) + match.Matches = func(actual map[K]V) bool { + return mapActualContainsExpected(expected, actual) + } + return match +} + +// MapContainingValues finds if all values V is contained as a value of actual[k] +// Acts like "ContainsAll", all elements given must be present in actual in the same order as the expected values. +func MapContainingValues[K comparable, V comparable](expected ...V) *gocrest.Matcher[map[K]V] { + match := new(gocrest.Matcher[map[K]V]) + match.Describe = fmt.Sprintf("something that contains %v", expected) + match.Matches = func(actual map[K]V) bool { + return mapActualContainsExpectedValues(expected, actual) + } + return match +} + +// MapMatchingValues finds if all values V is match a value of actual[k] +// Acts like "ContainsAll", all elements given must match in actual in the same order as the expected values. +func MapMatchingValues[K comparable, V comparable](expected ...*gocrest.Matcher[V]) *gocrest.Matcher[map[K]V] { + match := new(gocrest.Matcher[map[K]V]) + match.Describe = descriptionForMatchers(expected) + match.Matches = func(actual map[K]V) bool { + return mapActualMatchesExpected(expected, actual) + } + return match +} + +func descriptionForMatchers[A any](expected []*gocrest.Matcher[A]) string { + var description = "" + for x := 0; x < len(expected); x++ { + description += expected[x].Describe + if x < len(expected)-1 { + description += " and " } } - return len(contains) == expected.Len() + return description } -func mapContains(expected reflect.Value, actual reflect.Value) bool { - expectedKeys := expected.MapKeys() +// ArrayContaining finds if all x's are contained in y. +// Acts like "ContainsAll", all elements given must be present in actual in the same order as the expected values. +func ArrayContaining[A comparable](expected ...A) *gocrest.Matcher[[]A] { + match := new(gocrest.Matcher[[]A]) + match.Describe = fmt.Sprintf("something that contains %v", descriptionFor(expected)) + match.Matches = func(actual []A) bool { + return listContains(expected, actual) + } + return match +} - contains := make(map[interface{}]bool) +// ArrayMatching finds if all x's are matched in y. +// Acts like "ContainsAll", all elements given must be present in actual in the same order as the expected values. +func ArrayMatching[A comparable](expected ...*gocrest.Matcher[A]) *gocrest.Matcher[[]A] { + match := new(gocrest.Matcher[[]A]) + match.Describe = fmt.Sprintf("something that contains %v", descriptionFor(expected)) + match.Matches = func(actual []A) bool { + return listMatches(expected, actual) + } + return match +} +func mapActualContainsExpected[K comparable, V comparable](expected map[K]V, actual map[K]V) bool { + expectedKeys := make([]K, 0, len(expected)) + for k := range expected { + expectedKeys = append(expectedKeys, k) + } + contains := make(map[V]bool) for i := 0; i < len(expectedKeys); i++ { - val := actual.MapIndex(expectedKeys[i]) - if val.IsValid() { - if val.Interface() == expected.MapIndex(expectedKeys[i]).Interface() { - contains[val] = true + val := actual[expectedKeys[i]] + if val == expected[expectedKeys[i]] { + contains[val] = true + } + } + return len(contains) == len(expectedKeys) +} +func mapActualContainsExpectedValues[K comparable, V comparable](expected []V, actual map[K]V) bool { + contains := make(map[V]bool) + for i := 0; i < len(expected); i++ { + for k, v := range actual { + if actual[k] == expected[i] { + contains[v] = true + break } } } - return len(contains) == len(expected.MapKeys()) + return len(contains) == len(expected) } - -func listContains(expectedValue reflect.Value, actualValue reflect.Value) bool { +func mapActualMatchesExpected[K comparable, V comparable, A map[K]V](expected []*gocrest.Matcher[V], actual A) bool { + actualKeys := make([]K, 0, len(actual)) + for k := range actual { + actualKeys = append(actualKeys, k) + } contains := make(map[interface{}]bool) - for i := 0; i < expectedValue.Len(); i++ { - for y := 0; y < actualValue.Len(); y++ { - exp := expectedValue.Index(i).Interface() - act := actualValue.Index(y).Interface() - typeMatcher, ok := exp.(*gocrest.Matcher) - if ok { - if typeMatcher.Matches(act) { - contains[act] = true - } - } else { - if exp == act { - contains[act] = true - } + for i := 0; i < len(expected); i++ { + for _, v := range actual { + if expected[i].Matches(v) { + contains[v] = true } } } - return len(contains) == expectedValue.Len() + return len(contains) == len(expected) } -func correctExpectedValue(expected ...interface{}) interface{} { - kind := reflect.ValueOf(expected[0]).Kind() - if kind == reflect.Slice || kind == reflect.Map { - return expected[0] +func listContains[T comparable, A []T](expected A, actualValue A) bool { + contains := make(map[interface{}]bool) + for i := 0; i < len(expected); i++ { + for y := 0; y < len(actualValue); y++ { + exp := expected[i] + act := actualValue[y] + if exp == act { + contains[act] = true + } + } } - return expected + return len(contains) == len(expected) } - -func descriptionFor(expected ...interface{}) interface{} { - kind := reflect.ValueOf(expected[0]).Kind() - if kind == reflect.Slice || kind == reflect.Map { - return expected[0] +func listMatches[T comparable](expected []*gocrest.Matcher[T], actualValue []T) bool { + contains := make(map[interface{}]bool) + for i := 0; i < len(expected); i++ { + for y := 0; y < len(actualValue); y++ { + exp := expected[i] + act := actualValue[y] + if exp.Matches(act) { + contains[act] = true + } + } } + return len(contains) == len(expected) +} + +func descriptionFor[T any, A []T](expected A) string { var description = "" for x := 0; x < len(expected); x++ { - var matcher, ok = expected[x].(*gocrest.Matcher) - if ok { - description += matcher.Describe - } else { - description += fmt.Sprintf("%s", "<"+expected[x].(string)+">") - } + description += fmt.Sprintf("<%v>", expected[x]) if x < len(expected)-1 { description += " and " } diff --git a/is/describe.go b/is/describe.go index ee263a7..3aead55 100644 --- a/is/describe.go +++ b/is/describe.go @@ -5,7 +5,7 @@ import ( "github.com/corbym/gocrest" ) -func describe(conjunction string, matchers []*gocrest.Matcher) string { +func describe[A any](conjunction string, matchers []*gocrest.Matcher[A]) string { var description string for x := 0; x < len(matchers); x++ { description += matchers[x].Describe diff --git a/is/empty.go b/is/empty.go index b445f7b..2f8ce2f 100644 --- a/is/empty.go +++ b/is/empty.go @@ -2,28 +2,16 @@ package is import ( "github.com/corbym/gocrest" - "reflect" ) // Empty matches if the actual is "empty". // 'string' values are empty if they are "", maps, arrays and slices are empty if len(actual) is 0. -// Pointers and interfaces are empty when nil. -// Other types (int, float, bool) will cause the function to panic. // Returns a matcher that evaluates true if actual is "empty". -func Empty() *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func Empty[K comparable, T any, A string | []T | map[K]T]() *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) matcher.Describe = "empty value" - matcher.Matches = func(actual interface{}) bool { - if actual == nil { - return true - } - if actualValue, ok := actual.(string); ok { - return actualValue == "" - } - if reflect.ValueOf(actual).Len() == 0 { - return true - } - return false + matcher.Matches = func(actual A) bool { + return len(actual) == 0 } return matcher } diff --git a/is/equalto.go b/is/equalto.go index 4f90a31..caa3d78 100644 --- a/is/equalto.go +++ b/is/equalto.go @@ -1,28 +1,19 @@ package is import ( - "bytes" "fmt" "github.com/corbym/gocrest" "reflect" ) -// EqualTo checks if two values are equal. Uses DeepEqual (could be slow) or Compare for byte arrays. +// EqualTo checks if two values are equal. Uses DeepEqual (could be slow). // Like DeepEquals, if the types are not the same the matcher returns false. // Returns a matcher that will return true if two values are equal. -func EqualTo(expected interface{}) *gocrest.Matcher { - match := new(gocrest.Matcher) +func EqualTo[A any](expected A) *gocrest.Matcher[A] { + match := new(gocrest.Matcher[A]) match.Describe = fmt.Sprintf("value equal to <%v>", expected) - match.Matches = func(actual interface{}) bool { - switch actual.(type) { - case []byte: - expectedBytes := expected.([]byte) - actualBytes := actual.([]byte) - compare := bytes.Compare(expectedBytes, actualBytes) - return compare == 0 - default: - return reflect.DeepEqual(expected, actual) - } + match.Matches = func(actual A) bool { + return reflect.DeepEqual(expected, actual) } return match diff --git a/is/equaltoignoringwhitespace.go b/is/equaltoignoringwhitespace.go index 9713e79..0f88607 100644 --- a/is/equaltoignoringwhitespace.go +++ b/is/equaltoignoringwhitespace.go @@ -12,12 +12,11 @@ import ( // "a b c" is EqualToIgnoringWhitespace when compared with "a \nb \tc" // "ab\tc" is EqualToIgnoringWhitespace when compared with "a \nb \tc" // .. and so on. -func EqualToIgnoringWhitespace(expected string) (matcher *gocrest.Matcher) { - matcher = new(gocrest.Matcher) - matcher.Matches = func(actual interface{}) bool { - actualString := actual.(string) +func EqualToIgnoringWhitespace(expected string) (matcher *gocrest.Matcher[string]) { + matcher = new(gocrest.Matcher[string]) + matcher.Matches = func(actual string) bool { expectedFields := strings.Join(strings.Fields(expected), "") - actualFields := strings.Join(strings.Fields(actualString), "") + actualFields := strings.Join(strings.Fields(actual), "") equalToMatcher := EqualTo(expectedFields) matcher.Describe = "ignoring whitespace value equal to <" + expected + ">" diff --git a/is/greaterthan.go b/is/greaterthan.go index 5d3046b..3f97fea 100644 --- a/is/greaterthan.go +++ b/is/greaterthan.go @@ -3,37 +3,16 @@ package is import ( "fmt" "github.com/corbym/gocrest" - "reflect" ) // GreaterThan matcher compares two values that are numeric or string values, and when // called returns true if actual > expected. Strings are compared lexicographically with '>'. -// The matcher will always return false for unknown types. -// Actual and expected types must be the same underlying type, or the function will panic. // Returns a matcher that checks if actual is greater than expected. -func GreaterThan(expected interface{}) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func GreaterThan[A int | int8 | int16 | int32 | int64 | float32 | float64 | uint | uint16 | uint32 | uint64 | string](expected A) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) matcher.Describe = fmt.Sprintf("value greater than <%v>", expected) - matcher.Matches = func(actual interface{}) bool { - actualValue := reflect.ValueOf(actual) - expectedValue := reflect.ValueOf(expected) - switch expected.(type) { - case float32, float64: - { - return actualValue.Float() > expectedValue.Float() - } - case int, int8, int16, int32, int64: - { - return actualValue.Int() > expectedValue.Int() - } - case uint, uint8, uint16, uint32, uint64: - { - return actualValue.Uint() > expectedValue.Uint() - } - case string: - return actualValue.String() > expectedValue.String() - } - return false + matcher.Matches = func(actual A) bool { + return actual > expected } return matcher } diff --git a/is/greaterthanorequalto.go b/is/greaterthanorequalto.go index 23cc501..17f2018 100644 --- a/is/greaterthanorequalto.go +++ b/is/greaterthanorequalto.go @@ -2,11 +2,11 @@ package is import "github.com/corbym/gocrest" -// GreaterThanOrEqualTo is a short hand matcher for anyOf(greaterThan(x), equalTo(x)) +// GreaterThanOrEqualTo is a shorthand matcher for anyOf(greaterThan(x), equalTo(x)) // Returns a matcher matching if actual >= expected (using deepEquals). -func GreaterThanOrEqualTo(expected interface{}) *gocrest.Matcher { - matcher := new(gocrest.Matcher) - matcher.Matches = func(actual interface{}) bool { +func GreaterThanOrEqualTo[A int | int8 | int16 | int32 | int64 | float32 | float64 | uint | uint16 | uint32 | uint64 | string](expected A) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) + matcher.Matches = func(actual A) bool { anyOf := AnyOf(GreaterThan(expected), EqualTo(expected)) anyOfMatches := anyOf.Matches(actual) matcher.Describe = anyOf.Describe diff --git a/is/isfalse.go b/is/isfalse.go index 2095abb..06069e4 100644 --- a/is/isfalse.go +++ b/is/isfalse.go @@ -2,11 +2,11 @@ package is import "github.com/corbym/gocrest" -// False returns true if the actual matches false. Confusing but true. -func False() *gocrest.Matcher { - return &gocrest.Matcher{ +// False returns true if the actual equals false. Confusing but true. +func False() *gocrest.Matcher[bool] { + return &gocrest.Matcher[bool]{ Describe: "is false", - Matches: func(actual interface{}) bool { + Matches: func(actual bool) bool { return actual == false }, } diff --git a/is/istrue.go b/is/istrue.go index 3627bcf..d523b01 100644 --- a/is/istrue.go +++ b/is/istrue.go @@ -3,10 +3,10 @@ package is import "github.com/corbym/gocrest" // True returns true if the actual matches true -func True() *gocrest.Matcher { - return &gocrest.Matcher{ +func True[A bool]() *gocrest.Matcher[A] { + return &gocrest.Matcher[A]{ Describe: "is true", - Matches: func(actual interface{}) bool { + Matches: func(actual A) bool { return actual == true }, } diff --git a/is/lessthan.go b/is/lessthan.go index 2e55ac1..3c8f9e2 100644 --- a/is/lessthan.go +++ b/is/lessthan.go @@ -3,31 +3,16 @@ package is import ( "fmt" "github.com/corbym/gocrest" - "reflect" ) // LessThan matcher compares two values that are numeric or string values, and when // called returns true if actual < expected. Strings are compared lexicographically with '<'. -// The matcher will always return false for unknown types. -// Actual and expected types must be the same underlying type, or the function will panic. // Returns a matcher that checks if actual is greater than expected. -func LessThan(expected interface{}) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func LessThan[A int | int8 | int16 | int32 | int64 | float32 | float64 | uint | uint16 | uint32 | uint64 | string](expected A) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) matcher.Describe = fmt.Sprintf("value less than <%v>", expected) - matcher.Matches = func(actual interface{}) bool { - actualValue := reflect.ValueOf(actual) - expectedValue := reflect.ValueOf(expected) - switch expected.(type) { - case float32, float64: - return actualValue.Float() < expectedValue.Float() - case int, int8, int16, int32, int64: - return actualValue.Int() < expectedValue.Int() - case uint, uint8, uint16, uint32, uint64: - return actualValue.Uint() < expectedValue.Uint() - case string: - return actualValue.String() < expectedValue.String() - } - return false + matcher.Matches = func(actual A) bool { + return actual < expected } return matcher } diff --git a/is/lessthanorequalto.go b/is/lessthanorequalto.go index aa62321..f9c66dc 100644 --- a/is/lessthanorequalto.go +++ b/is/lessthanorequalto.go @@ -4,9 +4,9 @@ import "github.com/corbym/gocrest" // LessThanOrEqualTo is a short hand matcher for anyOf(lessThan(x), equalTo(x)) // Returns a matcher matching if actual <= expected (using deepEquals). -func LessThanOrEqualTo(expected interface{}) *gocrest.Matcher { - matcher := new(gocrest.Matcher) - matcher.Matches = func(actual interface{}) bool { +func LessThanOrEqualTo[A int | int8 | int16 | int32 | int64 | float32 | float64 | uint | uint16 | uint32 | uint64 | string](expected A) *gocrest.Matcher[A] { + matcher := new(gocrest.Matcher[A]) + matcher.Matches = func(actual A) bool { anyOf := AnyOf(LessThan(expected), EqualTo(expected)) anyOfMatches := anyOf.Matches(actual) matcher.Describe = anyOf.Describe diff --git a/is/matchespattern.go b/is/matchespattern.go index 70dd2cd..245a739 100644 --- a/is/matchespattern.go +++ b/is/matchespattern.go @@ -9,16 +9,16 @@ import ( // MatchForPattern matches if actual string matches the expected regex // String provided must be a valid for compilation with regexp.Compile. // Returns a matcher that uses the expected for a regex to match the actual value. -func MatchForPattern(expected string) *gocrest.Matcher { - matcher := new(gocrest.Matcher) +func MatchForPattern(expected string) *gocrest.Matcher[string] { + matcher := new(gocrest.Matcher[string]) matcher.Describe = fmt.Sprintf("a value that matches pattern %s", expected) - matcher.Matches = func(actual interface{}) bool { + matcher.Matches = func(actual string) bool { compiledExp, err := regexp.Compile(expected) if err != nil { matcher.Describe = err.Error() return false } - return compiledExp.MatchString(actual.(string)) + return compiledExp.MatchString(actual) } return matcher } diff --git a/is/nil.go b/is/nil.go index 123a27e..04a6e69 100644 --- a/is/nil.go +++ b/is/nil.go @@ -7,12 +7,12 @@ import ( ) // Nil matches if the actual value is nil -func Nil() *gocrest.Matcher { - match := new(gocrest.Matcher) +func Nil[A any]() *gocrest.Matcher[A] { + match := new(gocrest.Matcher[A]) match.Describe = "value that is " - match.Matches = func(actual interface{}) bool { + match.Matches = func(actual A) bool { match.Actual = fmt.Sprintf("%v", actual) - if actual == nil { + if any(actual) == nil { return true } switch reflect.TypeOf(actual).Kind() { diff --git a/is/not.go b/is/not.go index 7e2349e..b78ebef 100644 --- a/is/not.go +++ b/is/not.go @@ -6,10 +6,10 @@ import ( // Not negates the given matcher. // Returns a matcher that returns logical not of the matcher given. -func Not(matcher *gocrest.Matcher) *gocrest.Matcher { - match := new(gocrest.Matcher) +func Not[A any](matcher *gocrest.Matcher[A]) *gocrest.Matcher[A] { + match := new(gocrest.Matcher[A]) match.Describe = "not(" + matcher.Describe + ")" - match.Matches = func(actual interface{}) bool { + match.Matches = func(actual A) bool { matches := !matcher.Matches(actual) match.Actual = matcher.Actual return matches diff --git a/matcher.go b/matcher.go index 65b52c5..8771bda 100644 --- a/matcher.go +++ b/matcher.go @@ -6,9 +6,9 @@ import ( ) // Matcher provides the structure for matcher operations. -type Matcher struct { +type Matcher[A any] struct { // Matches returns true if the function matches. - Matches func(actual interface{}) bool + Matches func(actual A) bool // Describe describes the matcher (e.g. "a value EqualTo(foo)" - foo being the expected value) Describe string // Actual is used by then.AssertThat if the matcher @@ -21,24 +21,24 @@ type Matcher struct { } // Reason for the mismatch. -func (matcher *Matcher) Reason(r string) *Matcher { +func (matcher *Matcher[A]) Reason(r string) *Matcher[A] { matcher.ReasonString = r return matcher } // Describes the Matcher to conform to the Stringer interface -func (matcher *Matcher) String() string { +func (matcher *Matcher[A]) String() string { return matcher.Describe } // Reasonf allows a formatted reason for the mismatch. -func (matcher *Matcher) Reasonf(format string, args ...interface{}) *Matcher { +func (matcher *Matcher[A]) Reasonf(format string, args ...interface{}) *Matcher[A] { return matcher.Reason(fmt.Sprintf(format, args...)) } // AppendActual appends an actual string to the matcher's actual description. This is useful if you want // to preserve sub-matchers actual values. See is.AllOf() matcher for an example. -func (matcher *Matcher) AppendActual(actualAsString string) { +func (matcher *Matcher[A]) AppendActual(actualAsString string) { matcher.Actual += " " + actualAsString matcher.Actual = strings.TrimSpace(matcher.Actual) } diff --git a/matcher_test.go b/matcher_test.go index 5811088..b074af6 100644 --- a/matcher_test.go +++ b/matcher_test.go @@ -20,22 +20,53 @@ func init() { stubTestingT = new(StubTestingT) } -func TestHasLengthMatchesOrNot(testing *testing.T) { +func TestEqualTo(t *testing.T) { + then.AssertThat(t, "foo", is.EqualTo("foo")) + then.AssertThat(t, 1, is.EqualTo(1)) + then.AssertThat(t, []string{"a"}, is.EqualTo([]string{"a"})) +} + +func TestMixedMatcher(t *testing.T) { + then.AssertThat(t, "abcdef", is.AllOf(is.StringContaining("abc"), is.LessThan("ghi"))) +} + +func TestNil(testing *testing.T) { + values := struct { + actual *string + }{ + actual: nil, + } + then.AssertThat(testing, values.actual, is.Nil[*string]()) +} +func TestHasLengthStringMatchesOrNot(testing *testing.T) { var hasLengthItems = []struct { - actual interface{} - expected interface{} + actual string + expected int shouldFail bool }{ - {actual: nil, expected: nil, shouldFail: true}, {actual: "", expected: 0, shouldFail: false}, {actual: "a", expected: 1, shouldFail: false}, {actual: "a", expected: 1, shouldFail: false}, {actual: "1", expected: 1, shouldFail: false}, - {actual: []string{}, expected: 0, shouldFail: false}, - {actual: []string{"foo"}, expected: 1, shouldFail: false}, - {actual: []string{"foo"}, expected: 2, shouldFail: true}, - {actual: []string{"foo", "bar"}, expected: 2, shouldFail: false}, - {actual: map[string]bool{"hello": true}, expected: 1, shouldFail: false}, + } + for _, test := range hasLengthItems { + stubTestingT = new(StubTestingT) + then.AssertThat(testing, "a", has.Length[string, string](1)) + then.AssertThat(stubTestingT, test.actual, has.Length[string, string](test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, has.Length(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestMapHasLengthOne(t *testing.T) { + then.AssertThat(t, map[string]bool{"hello": true}, has.MapLength[string, bool](1)) +} +func TestHasLengthMapMatchesOrNot(testing *testing.T) { + var hasLengthItems = []struct { + actual map[string]bool + expected *gocrest.Matcher[int] + shouldFail bool + }{ {actual: map[string]bool{"helloa": true}, expected: is.LessThan(1), shouldFail: true}, {actual: map[string]bool{"hellob": true}, expected: is.LessThanOrEqualTo(2), shouldFail: false}, {actual: map[string]bool{"helloc": true}, expected: is.GreaterThan(2), shouldFail: true}, @@ -43,7 +74,65 @@ func TestHasLengthMatchesOrNot(testing *testing.T) { } for _, test := range hasLengthItems { stubTestingT = new(StubTestingT) - then.AssertThat(stubTestingT, test.actual, has.Length(test.expected)) + then.AssertThat(stubTestingT, test.actual, has.MapLengthMatching[string, bool](test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, has.Length(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestHasLengthArrayMatchesOrNot(testing *testing.T) { + var hasLengthItems = []struct { + actual []int + expected int + shouldFail bool + }{ + {actual: []int{}, expected: 0, shouldFail: false}, + {actual: []int{1}, expected: 1, shouldFail: false}, + {actual: []int{1}, expected: 2, shouldFail: true}, + {actual: []int{1, 2}, expected: 2, shouldFail: false}, + } + for _, test := range hasLengthItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, has.Length[int, []int](test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, has.Length(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestHasLengthMatchesArrayMatchesOrNot(testing *testing.T) { + var hasLengthItems = []struct { + actual []int + expected int + shouldFail bool + }{ + {actual: []int{}, expected: 0, shouldFail: false}, + {actual: []int{1}, expected: 1, shouldFail: false}, + {actual: []int{1}, expected: 2, shouldFail: true}, + {actual: []int{1, 2}, expected: 2, shouldFail: false}, + } + for _, test := range hasLengthItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, has.LengthMatching[int, []int](is.EqualTo(test.expected))) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, has.Length(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestHasLengthArrayStringMatchesOrNot(testing *testing.T) { + var hasLengthItems = []struct { + actual []string + expected int + shouldFail bool + }{ + {actual: nil, expected: 2, shouldFail: true}, + {actual: []string{}, expected: 0, shouldFail: false}, + {actual: []string{"foo"}, expected: 1, shouldFail: false}, + {actual: []string{"foo"}, expected: 2, shouldFail: true}, + {actual: []string{"foo", "bar"}, expected: 2, shouldFail: false}, + } + for _, test := range hasLengthItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, has.Length[string, []string](test.expected)) if stubTestingT.HasFailed() != test.shouldFail { testing.Errorf("assertThat(%v, has.Length(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) } @@ -52,8 +141,8 @@ func TestHasLengthMatchesOrNot(testing *testing.T) { func TestAssertThatTwoValuesAreEqualOrNot(testing *testing.T) { var equalsItems = []struct { - actual interface{} - expected interface{} + actual any + expected any shouldFail bool }{ {actual: 1, expected: 1, shouldFail: false}, @@ -73,49 +162,73 @@ func TestAssertThatTwoValuesAreEqualOrNot(testing *testing.T) { func TestEmptyStringIsEmptyPasses(testing *testing.T) { var equalsItems = []struct { - actual interface{} + actual string shouldFail bool }{ {actual: "hi", shouldFail: true}, - {actual: nil, shouldFail: false}, {actual: "", shouldFail: false}, + } + for _, test := range equalsItems { + stubTestingT := new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, is.Empty[string, string, string]()) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, Empty()) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestEmptyMapIsEmptyPasses(testing *testing.T) { + var equalsItems = []struct { + actual map[string]bool + shouldFail bool + }{ {actual: map[string]bool{"hello": true}, shouldFail: true}, {actual: map[string]bool{}, shouldFail: false}, + } + for _, test := range equalsItems { + stubTestingT := new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, is.Empty[string, bool, map[string]bool]()) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, Empty()) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestEmptyArrayIsEmptyPasses(testing *testing.T) { + var equalsItems = []struct { + actual []string + shouldFail bool + }{ {actual: []string{}, shouldFail: false}, {actual: []string{"boo"}, shouldFail: true}, } for _, test := range equalsItems { stubTestingT := new(StubTestingT) - then.AssertThat(stubTestingT, test.actual, is.Empty()) + then.AssertThat(stubTestingT, test.actual, is.Empty[string, string, []string]()) if stubTestingT.HasFailed() != test.shouldFail { testing.Errorf("assertThat(%v, Empty()) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.shouldFail, stubTestingT.HasFailed()) } } } -func TestAssertThatTwoValuesAreGreaterThanOrNot(testing *testing.T) { +func TestAssertThatTwoIntValuesAreGreaterThanOrNotFails(testing *testing.T) { + then.AssertThat(stubTestingT, 1, is.GreaterThan(1)) + hasFailed(testing) + then.AssertThat(stubTestingT, 1.12, is.GreaterThan(1.12)) + hasFailed(testing) +} + +func hasFailed(testing *testing.T) { + if !stubTestingT.HasFailed() { + testing.Errorf(testing.Name()) + } +} +func TestAssertThatTwoValuesAreGreaterThanOrNotFails(testing *testing.T) { var equalsItems = []struct { - actual interface{} - expected interface{} + actual string + expected string shouldFail bool }{ - {actual: 1, expected: 1, shouldFail: true}, - {actual: 2, expected: 1, shouldFail: false}, - {actual: 1.12, expected: 1.12, shouldFail: true}, - {actual: float32(1.12), expected: float32(1.0), shouldFail: false}, - {actual: float64(1.24), expected: float64(1.0), shouldFail: false}, - {actual: uint(3), expected: uint(1), shouldFail: false}, - {actual: uint16(4), expected: uint16(1), shouldFail: false}, - {actual: uint32(6), expected: uint32(1), shouldFail: false}, - {actual: uint64(7), expected: uint64(1), shouldFail: false}, - {actual: uint64(8), expected: uint64(1), shouldFail: false}, - {actual: int16(9), expected: int16(1), shouldFail: false}, - {actual: int32(10), expected: int32(1), shouldFail: false}, - {actual: int64(11), expected: int64(1), shouldFail: false}, - {actual: int64(12), expected: int64(1), shouldFail: false}, {actual: "zzz", expected: "aaa", shouldFail: false}, {actual: "aaa", expected: "zzz", shouldFail: true}, - {actual: complex64(1.0), expected: complex64(1.0), shouldFail: true}, } for _, test := range equalsItems { stubTestingT = new(StubTestingT) @@ -125,73 +238,108 @@ func TestAssertThatTwoValuesAreGreaterThanOrNot(testing *testing.T) { } } } +func TestAssertThatTwoFloatValuesAreGreaterThanOrNot(testing *testing.T) { + then.AssertThat(testing, float32(1.12), is.GreaterThan(float32(1.0))) + then.AssertThat(testing, 2, is.GreaterThan(1)) + then.AssertThat(testing, 1.24, is.GreaterThan(1.0)) +} +func TestAssertThatTwoIntsValuesAreGreaterThanOrNot(testing *testing.T) { + then.AssertThat(testing, uint(3), is.GreaterThan(uint(1))) + then.AssertThat(testing, uint16(4), is.GreaterThan(uint16(1))) + then.AssertThat(testing, uint32(6), is.GreaterThan(uint32(1))) + then.AssertThat(testing, uint64(7), is.GreaterThan(uint64(1))) + then.AssertThat(testing, uint64(8), is.GreaterThan(uint64(1))) + then.AssertThat(testing, int16(9), is.GreaterThan(int16(1))) + then.AssertThat(testing, int32(10), is.GreaterThan(int32(1))) + then.AssertThat(testing, int64(11), is.GreaterThan(int64(1))) + then.AssertThat(testing, int64(12), is.GreaterThan(int64(1))) +} func TestAssertThatHasLengthFailsWithDescriptionTest(testing *testing.T) { - then.AssertThat(stubTestingT, "a", has.Length(2)) + then.AssertThat(stubTestingT, "a", has.Length[string, string](2)) if !strings.Contains(stubTestingT.MockTestOutput, "value with length 2") { testing.Errorf("did not get expected description, got: %s", stubTestingT.MockTestOutput) } } -func TestAssertThatTwoValuesAreGreaterThanOrEqualToOrNot(testing *testing.T) { +func TestAssertThatTwoIntsAreLessThanOrNot(testing *testing.T) { var equalsItems = []struct { - actual interface{} - expected interface{} + actual int + expected int shouldFail bool }{ - {actual: 1, expected: 1, shouldFail: false}, - {actual: 2, expected: 1, shouldFail: false}, - {actual: 1.12, expected: 1.12, shouldFail: false}, - {actual: 1.0, expected: 1.12, shouldFail: true}, - {actual: float32(1.12), expected: float32(1.0), shouldFail: false}, - {actual: float64(1.24), expected: float64(1.0), shouldFail: false}, - {actual: float64(0.5), expected: float64(1.0), shouldFail: true}, - {actual: uint(1), expected: uint(1), shouldFail: false}, - {actual: uint(3), expected: uint(1), shouldFail: false}, - {actual: uint16(4), expected: uint16(1), shouldFail: false}, - {actual: uint32(6), expected: uint32(1), shouldFail: false}, - {actual: uint64(7), expected: uint64(1), shouldFail: false}, - {actual: uint64(8), expected: uint64(1), shouldFail: false}, - {actual: int16(9), expected: int16(1), shouldFail: false}, - {actual: int32(10), expected: int32(1), shouldFail: false}, - {actual: int64(11), expected: int64(1), shouldFail: false}, - {actual: int64(12), expected: int64(1), shouldFail: false}, - {actual: "zzz", expected: "aaa", shouldFail: false}, - {actual: "aaa", expected: "zzz", shouldFail: true}, - {actual: "aaa", expected: "aaa", shouldFail: false}, - {actual: complex64(1.0), expected: complex64(1.0), shouldFail: false}, + {actual: 1, expected: 1, shouldFail: true}, + {actual: 1, expected: 2, shouldFail: false}, } for _, test := range equalsItems { stubTestingT = new(StubTestingT) - then.AssertThat(stubTestingT, test.actual, is.GreaterThanOrEqualTo(test.expected)) + then.AssertThat(stubTestingT, test.actual, is.LessThan(test.expected)) if stubTestingT.HasFailed() != test.shouldFail { - testing.Errorf("assertThat(%v, GreaterThan(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + testing.Errorf("assertThat(%v, LessThan(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) } } } - -func TestAssertThatTwoValuesAreLessThanOrNot(testing *testing.T) { +func TestAssertThatTwoFloat32ValuesAreLessThanOrNot(testing *testing.T) { var equalsItems = []struct { - actual interface{} - expected interface{} + actual float32 + expected float32 shouldFail bool }{ - {actual: 1, expected: 1, shouldFail: true}, - {actual: 1, expected: 2, shouldFail: false}, {actual: 1.12, expected: 1.12, shouldFail: true}, {actual: float32(1.0), expected: float32(1.12), shouldFail: false}, - {actual: float64(1.0), expected: float64(1.24), shouldFail: false}, - {actual: uint(0), expected: uint(3), shouldFail: false}, + } + for _, test := range equalsItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, is.LessThan(test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, LessThan(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestAssertThatTwoFloat64sAreLessThanOrNot(testing *testing.T) { + var equalsItems = []struct { + actual float64 + expected float64 + shouldFail bool + }{ + {actual: 1.12, expected: 1.12, shouldFail: true}, + {actual: 1.0, expected: 1.24, shouldFail: false}, + } + for _, test := range equalsItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, is.LessThan(test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, LessThan(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestAssertThatTwoUintsAreLessThanOrNot(testing *testing.T) { + var equalsItems = []struct { + actual uint16 + expected uint16 + shouldFail bool + }{ + {actual: 1, expected: 1, shouldFail: true}, + {actual: 1, expected: 2, shouldFail: false}, {actual: uint16(1), expected: uint16(4), shouldFail: false}, - {actual: uint32(1), expected: uint32(6), shouldFail: false}, + } + for _, test := range equalsItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, is.LessThan(test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, LessThan(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestAssertThatTwoUints64sAreLessThanOrNot(testing *testing.T) { + var equalsItems = []struct { + actual uint64 + expected uint64 + shouldFail bool + }{ + {actual: 1, expected: 1, shouldFail: true}, + {actual: 1, expected: 2, shouldFail: false}, {actual: uint64(1), expected: uint64(7), shouldFail: false}, {actual: uint64(1), expected: uint64(8), shouldFail: false}, - {actual: int16(1), expected: int16(9), shouldFail: false}, - {actual: int32(1), expected: int32(10), shouldFail: false}, - {actual: int64(1), expected: int64(11), shouldFail: false}, - {actual: "aaa", expected: "zzz", shouldFail: false}, - {actual: "zzz", expected: "aaa", shouldFail: true}, - {actual: "aaa", expected: "aaa", shouldFail: true}, - {actual: complex64(1.0), expected: complex64(1.0), shouldFail: true}, // cannot compare complex types, so fails } for _, test := range equalsItems { stubTestingT = new(StubTestingT) @@ -202,32 +350,69 @@ func TestAssertThatTwoValuesAreLessThanOrNot(testing *testing.T) { } } -func TestAssertThatTwoValuesAreLessThanOrEqualToPassesOrNot(testing *testing.T) { +func TestAssertThatTwoIntValuesAreLessThanOrEqualToPassesOrNot(testing *testing.T) { var equalsItems = []struct { - actual interface{} - expected interface{} + actual int16 + expected int16 shouldFail bool }{ {actual: 1, expected: 1, shouldFail: false}, {actual: 1, expected: 2, shouldFail: false}, - {actual: 1.12, expected: 1.12, shouldFail: false}, - {actual: 2.3, expected: 1.12, shouldFail: true}, - {actual: float32(1.0), expected: float32(1.12), shouldFail: false}, - {actual: float64(1.0), expected: float64(1.24), shouldFail: false}, - {actual: float64(1.0), expected: float64(1.0), shouldFail: false}, - {actual: float64(1.0), expected: float64(0.5), shouldFail: true}, - {actual: uint(0), expected: uint(0), shouldFail: false}, - {actual: uint16(1), expected: uint16(4), shouldFail: false}, - {actual: uint32(1), expected: uint32(6), shouldFail: false}, - {actual: uint64(1), expected: uint64(7), shouldFail: false}, - {actual: uint64(1), expected: uint64(8), shouldFail: false}, {actual: int16(1), expected: int16(9), shouldFail: false}, + } + for _, test := range equalsItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, is.LessThanOrEqualTo(test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, LessThan(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestAssertThatTwoInt32ValuesAreLessThanOrEqualToPassesOrNot(testing *testing.T) { + var equalsItems = []struct { + actual int32 + expected int32 + shouldFail bool + }{ + {actual: 1, expected: 1, shouldFail: false}, + {actual: 1, expected: 2, shouldFail: false}, {actual: int32(1), expected: int32(10), shouldFail: false}, + } + for _, test := range equalsItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, is.LessThanOrEqualTo(test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, LessThan(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestAssertThatTwoInt64ValuesAreLessThanOrEqualToPassesOrNot(testing *testing.T) { + var equalsItems = []struct { + actual int64 + expected int64 + shouldFail bool + }{ + {actual: 1, expected: 1, shouldFail: false}, + {actual: 1, expected: 2, shouldFail: false}, {actual: int64(1), expected: int64(11), shouldFail: false}, + } + for _, test := range equalsItems { + stubTestingT = new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, is.LessThanOrEqualTo(test.expected)) + if stubTestingT.HasFailed() != test.shouldFail { + testing.Errorf("assertThat(%v, LessThan(%v)) gave unexpected test result (wanted failed %v, got failed %v)", test.actual, test.expected, test.shouldFail, stubTestingT.HasFailed()) + } + } +} +func TestAssertThatTwoStringValuesAreLessThanOrEqualToPassesOrNot(testing *testing.T) { + var equalsItems = []struct { + actual string + expected string + shouldFail bool + }{ {actual: "aaa", expected: "zzz", shouldFail: false}, {actual: "zzz", expected: "aaa", shouldFail: true}, {actual: "aaa", expected: "aaa", shouldFail: false}, - {actual: complex64(1.0), expected: complex64(1.0), shouldFail: false}, } for _, test := range equalsItems { stubTestingT = new(StubTestingT) @@ -246,32 +431,33 @@ func TestNotReturnsTheOppositeOfGivenMatcher(testing *testing.T) { } func TestNotReturnsTheSubMatcherActual(testing *testing.T) { - not := is.Not(has.Length(1)) + not := is.Not[string](has.Length[string, string](1)) not.Matches("a") then.AssertThat(testing, not.Actual, is.EqualTo("length was 1")) } func TestAnyofReturnsTheSubMatcherActual(testing *testing.T) { - anyOf := is.AnyOf(has.Length(1), is.EqualTo("a")) + anyOf := is.AnyOf[string](has.Length[string, string](1), is.EqualTo("a")) anyOf.Matches("a") then.AssertThat(testing, anyOf.Actual, is.EqualTo("actual length was 1")) } func TestAllofReturnsTheSubMatcherActual(testing *testing.T) { - anyOf := is.AllOf(has.Length(1), is.EqualTo("a")) + anyOf := is.AllOf[string](has.Length[string, string](1), is.EqualTo("a")) anyOf.Matches("a") then.AssertThat(testing, anyOf.Actual, is.EqualTo("actual length was 1")) } func TestIsNilMatches(testing *testing.T) { - then.AssertThat(testing, nil, is.Nil()) + then.AssertThat(testing, nil, is.Nil[any]()) } func TestIsNilFails(testing *testing.T) { - then.AssertThat(stubTestingT, 2, is.Nil()) + var actual = 2 + then.AssertThat(stubTestingT, &actual, is.Nil[*int]()) if !stubTestingT.HasFailed() { testing.Fail() } @@ -280,7 +466,7 @@ func TestIsNilFails(testing *testing.T) { func TestContainsFailsForTwoStringArraysTest(testing *testing.T) { actualList := []string{"Foo", "Bar"} expectedList := []string{"Baz", "Bing"} - then.AssertThat(stubTestingT, actualList, is.ValueContaining(expectedList)) + then.AssertThat(stubTestingT, actualList, is.ArrayContaining(expectedList...)) if !stubTestingT.HasFailed() { testing.Fail() } @@ -289,7 +475,7 @@ func TestContainsFailsForTwoStringArraysTest(testing *testing.T) { func TestContainsFailsForTwoIntArraysTest(testing *testing.T) { actualList := []int{12, 13} expectedList := []int{14, 15} - then.AssertThat(stubTestingT, actualList, is.ValueContaining(expectedList)) + then.AssertThat(stubTestingT, actualList, is.ArrayContaining(expectedList...)) if !stubTestingT.HasFailed() { testing.Fail() } @@ -298,13 +484,13 @@ func TestContainsFailsForTwoIntArraysTest(testing *testing.T) { func TestContainsForString(testing *testing.T) { actualList := []string{"Foo", "Bar"} expected := "Foo" - then.AssertThat(testing, actualList, is.ValueContaining(expected)) + then.AssertThat(testing, actualList, is.ArrayContaining(expected)) } func TestContainsFailsForString(testing *testing.T) { actualList := []string{"Foo", "Bar"} expected := "Moo" - then.AssertThat(stubTestingT, actualList, is.ValueContaining(expected)) + then.AssertThat(stubTestingT, actualList, is.ArrayContaining(expected)) if !stubTestingT.HasFailed() { testing.Fail() } @@ -313,23 +499,23 @@ func TestContainsFailsForString(testing *testing.T) { func TestContainsForSlice(testing *testing.T) { actualList := []string{"Foo", "Bar", "Bong", "Boom"} expected := []string{"Baz", "Bing", "Bong"} - then.AssertThat(testing, actualList[2:2], is.ValueContaining(expected[2:2])) + then.AssertThat(testing, actualList[2:2], is.ArrayContaining(expected[2:2]...)) } func TestContainsForList(testing *testing.T) { actualList := []string{"Foo", "Bar", "Bong", "Boom"} expected := []string{"Boom", "Bong", "Bar"} - then.AssertThat(testing, actualList, is.ValueContaining(expected)) + then.AssertThat(testing, actualList, is.ArrayContaining(expected...)) } func TestContainsForVariadic(testing *testing.T) { actualList := []string{"Foo", "Bar", "Bong", "Boom"} - then.AssertThat(testing, actualList, is.ValueContaining("Boom", "Bong", "Bar")) + then.AssertThat(testing, actualList, is.ArrayContaining("Boom", "Bong", "Bar")) } func TestContainsForVariadicMatchers(testing *testing.T) { actualList := []string{"Foo", "Bar", "Bong", "Boom"} - then.AssertThat(testing, actualList, is.ValueContaining(is.EqualTo("Boom"), has.Suffix("ng"), has.Prefix("Ba"))) + then.AssertThat(testing, actualList, is.ArrayMatching(is.EqualTo("Boom"), has.Suffix("ng"), has.Prefix("Ba"))) } func TestMapContainsMap(testing *testing.T) { @@ -341,7 +527,7 @@ func TestMapContainsMap(testing *testing.T) { "bing": "boop", } - then.AssertThat(testing, actualList, is.ValueContaining(expected)) + then.AssertThat(testing, actualList, is.MapContaining(expected)) } func TestMapContainsValues(testing *testing.T) { @@ -349,33 +535,21 @@ func TestMapContainsValues(testing *testing.T) { "bing": "boop", "bling": "bling", } - then.AssertThat(testing, actualList, is.ValueContaining("bling", "boop")) + then.AssertThat(testing, actualList, is.MapContainingValues[string]("boop", "bling")) } -func TestMapContainsValuesMatching(testing *testing.T) { +func TestMapMatchesValues(testing *testing.T) { actualList := map[string]string{ "bing": "boop", "bling": "bling", } - then.AssertThat(testing, actualList, is.ValueContaining(is.EqualTo("bling"), is.EqualTo("boop"))) + then.AssertThat(testing, actualList, is.MapMatchingValues[string](is.EqualTo("boop"), is.EqualTo("bling"))) } func TestStringContains_String(testing *testing.T) { actualList := "abcd" expected := "bc" - then.AssertThat(testing, actualList, is.ValueContaining(expected)) -} - -func TestValueContaining_PanicsWithUnknownType(testing *testing.T) { - defer func() { - recover := recover() - then.AssertThat(testing, recover, is.Not(is.Nil())) - }() - expected := "abcd" - actualList := make(chan string, 3) - actualList <- "abc" - - then.AssertThat(testing, actualList, is.ValueContaining(expected)) + then.AssertThat(testing, actualList, is.StringContaining(expected)) } func TestMatchesPatternMatchesString(testing *testing.T) { @@ -432,7 +606,7 @@ func TestHasFunctionPasses(testing *testing.T) { } actual := new(MyType) expected := "f" - then.AssertThat(testing, actual, has.FunctionNamed(expected)) + then.AssertThat(testing, actual, has.FunctionNamed[*MyType](expected)) } func TestHasFunctionDoesNotPass(testing *testing.T) { @@ -441,7 +615,7 @@ func TestHasFunctionDoesNotPass(testing *testing.T) { } actual := new(MyType) expected := "E" - then.AssertThat(stubTestingT, actual, has.FunctionNamed(expected)) + then.AssertThat(stubTestingT, actual, has.FunctionNamed[*MyType](expected)) if !stubTestingT.HasFailed() { testing.Fail() } @@ -453,7 +627,7 @@ func TestHasFieldNamedPasses(testing *testing.T) { } actual := new(T) expected := "f" - then.AssertThat(testing, actual, has.FieldNamed(expected)) + then.AssertThat(testing, actual, has.FieldNamed[*T](expected)) } func TestHasFieldDoesNotPass(testing *testing.T) { @@ -462,7 +636,7 @@ func TestHasFieldDoesNotPass(testing *testing.T) { } actual := new(T) expected := "E" - then.AssertThat(stubTestingT, actual, has.FieldNamed(expected)) + then.AssertThat(stubTestingT, actual, has.FieldNamed[*T](expected)) if !stubTestingT.HasFailed() { testing.Fail() } @@ -470,12 +644,12 @@ func TestHasFieldDoesNotPass(testing *testing.T) { func TestAllOfMatches(testing *testing.T) { actual := "abcdef" - then.AssertThat(testing, actual, is.AllOf(is.EqualTo("abcdef"), is.ValueContaining("e"))) + then.AssertThat(testing, actual, is.AllOf(is.EqualTo("abcdef"), is.StringContaining("e"))) } func TestAllOfFailsToMatch(testing *testing.T) { actual := "abc" - then.AssertThat(stubTestingT, actual, is.AllOf(is.EqualTo("abc"), is.ValueContaining("e"))) + then.AssertThat(stubTestingT, actual, is.AllOf(is.EqualTo("abc"), is.StringContaining("e"))) if !stubTestingT.HasFailed() { testing.Fail() } @@ -483,12 +657,12 @@ func TestAllOfFailsToMatch(testing *testing.T) { func TestAnyOfMatches(testing *testing.T) { actual := "abcdef" - then.AssertThat(testing, actual, is.AnyOf(is.EqualTo("abcdef"), is.ValueContaining("g"))) + then.AssertThat(testing, actual, is.AnyOf(is.EqualTo("abcdef"), is.StringContaining("g"))) } func TestAnyOfFailsToMatch(testing *testing.T) { actual := "abc" - then.AssertThat(stubTestingT, actual, is.AnyOf(is.EqualTo("efg"), is.ValueContaining("e"))) + then.AssertThat(stubTestingT, actual, is.AnyOf(is.EqualTo("efg"), is.StringContaining("e"))) if !stubTestingT.HasFailed() { testing.Fail() } @@ -498,17 +672,15 @@ func TestHasKeyMatches(testing *testing.T) { type T struct{} expectedT := new(T) var equalsItems = []struct { - actual interface{} - expected interface{} + actual map[*T]bool + expected *T shouldFail bool }{ - {actual: map[string]bool{"hi": true}, expected: "hi", shouldFail: false}, - {actual: map[*T]bool{expectedT: true}, expected: "hi", shouldFail: true}, {actual: map[*T]bool{expectedT: true}, expected: expectedT, shouldFail: false}, } for _, test := range equalsItems { stubTestingT := new(StubTestingT) - then.AssertThat(stubTestingT, test.actual, has.Key(test.expected)) + then.AssertThat(stubTestingT, test.actual, has.Key[*T, bool](test.expected)) if stubTestingT.HasFailed() != test.shouldFail { testing.Errorf("unexpected result HasKey: wanted fail was %v but failed %v", test.shouldFail, stubTestingT.HasFailed()) } @@ -516,62 +688,132 @@ func TestHasKeyMatches(testing *testing.T) { } func TestHasKeysMatches(testing *testing.T) { + then.AssertThat(testing, map[string]bool{"hi": true, "bye": true}, has.AllKeys[string, bool]("hi", "bye")) type T struct{} expectedT := new(T) secondExpectedT := new(T) + then.AssertThat(testing, map[*T]bool{expectedT: true, secondExpectedT: true}, has.AllKeys[*T, bool](expectedT, secondExpectedT)) +} + +func TestBoolMatcherDescription(t *testing.T) { var equalsItems = []struct { - actual interface{} - expected interface{} - shouldFail bool + description string + actual bool + matcher *gocrest.Matcher[bool] + expected string }{ - {actual: map[string]bool{"hi": true, "bye": true}, expected: []string{"hi", "bye"}, shouldFail: false}, - {actual: map[*T]bool{expectedT: true, secondExpectedT: true}, expected: []*T{expectedT, secondExpectedT}, shouldFail: false}, - {actual: map[*T]bool{expectedT: true}, expected: "foo", shouldFail: true}, + {description: "is true", actual: false, matcher: is.True(), expected: "is true"}, + {description: "is false", actual: true, matcher: is.False(), expected: "is false"}, } for _, test := range equalsItems { - stubTestingT := new(StubTestingT) - then.AssertThat(stubTestingT, test.actual, has.AllKeys(test.expected)) - if stubTestingT.HasFailed() != test.shouldFail { - testing.Errorf("unexpected result HasKeys(%v): wanted fail was %v but failed %v", test.actual, test.shouldFail, stubTestingT.HasFailed()) - } + t.Run(test.description, func(innerT *testing.T) { + stubTestingT := new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, test.matcher) + if !strings.Contains(stubTestingT.MockTestOutput, test.expected) { + innerT.Errorf("%s did not fail with expected desc <%s>, got: %s", test.description, test.expected, stubTestingT.MockTestOutput) + } + }) } } - -func TestHasKeysWithVariadic(testing *testing.T) { - actual := map[string]bool{"hi": true, "bye": false} - then.AssertThat(testing, actual, has.AllKeys("hi", "bye")) +func TestTypeNameMatcherDescription(t *testing.T) { + var equalsItems = []struct { + description string + actual *testing.T + matcher *gocrest.Matcher[*testing.T] + expected string + }{ + {description: "has type T", actual: t, matcher: has.TypeName[*testing.T]("string"), expected: "has type "}, + {description: "has type T matcher", actual: t, matcher: has.TypeNameMatches[*testing.T](is.EqualTo("string")), expected: "has type value equal to "}, + } + for _, test := range equalsItems { + t.Run(test.description, func(innerT *testing.T) { + stubTestingT := new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, test.matcher) + if !strings.Contains(stubTestingT.MockTestOutput, test.expected) { + innerT.Errorf("%s did not fail with expected desc <%s>, got: %s", test.description, test.expected, stubTestingT.MockTestOutput) + } + }) + } } - -func TestMatcherDescription(t *testing.T) { +func TestSizeMatcherDescription(t *testing.T) { var equalsItems = []struct { description string - actual interface{} - matcher *gocrest.Matcher + actual int + matcher *gocrest.Matcher[int] expected string }{ - {description: "is true", actual: false, matcher: is.True(), expected: "is true"}, - {description: "is false", actual: true, matcher: is.False(), expected: "is false"}, - {description: "has type T", actual: t, matcher: has.TypeName("string"), expected: "has type "}, - {description: "has type T matcher", actual: t, matcher: has.TypeName(is.EqualTo("string")), expected: "has type value equal to "}, {description: "EqualTo.Reasonf", actual: 1, matcher: is.EqualTo(2).Reasonf("arithmetic %s is wrong", "foo"), expected: "arithmetic foo is wrong"}, {description: "EqualTo.Reason", actual: 1, matcher: is.EqualTo(2).Reason("arithmetic is wrong"), expected: "arithmetic is wrong\nExpected: value equal to <2>\n but: <1>\n"}, {description: "Not", actual: 2, matcher: is.Not(is.EqualTo(2)), expected: "\nExpected: not(value equal to <2>)\n but: <2>\n"}, - {description: "Empty", actual: map[string]bool{"foo": true}, matcher: is.Empty(), expected: "empty value"}, {description: "GreaterThan", actual: 1, matcher: is.GreaterThan(2), expected: "value greater than <2>"}, {description: "GreaterThanOrEqual", actual: 1, matcher: is.GreaterThanOrEqualTo(2), expected: "any of (value greater than <2> or value equal to <2>)"}, {description: "LessThan", actual: 2, matcher: is.LessThan(1), expected: "value less than <1>"}, {description: "LessThanOrEqualTo", actual: 2, matcher: is.LessThanOrEqualTo(1), expected: "any of (value less than <1> or value equal to <1>)"}, - {description: "Nil", actual: 1, matcher: is.Nil(), expected: "value that is "}, - {description: "ValueContaining", actual: []string{"Foo", "Bar"}, matcher: is.ValueContaining([]string{"Baz", "Bing"}), expected: "something that contains [Baz Bing]"}, - {description: "ValueContaining", actual: []string{"Foo", "Bar"}, matcher: is.ValueContaining(is.EqualTo("Baz"), is.EqualTo("Bing")), expected: "something that contains value equal to and value equal to "}, + } + for _, test := range equalsItems { + t.Run(test.description, func(innerT *testing.T) { + stubTestingT := new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, test.matcher) + if !strings.Contains(stubTestingT.MockTestOutput, test.expected) { + innerT.Errorf("%s did not fail with expected desc <%s>, got: %s", test.description, test.expected, stubTestingT.MockTestOutput) + } + }) + } +} +func TestSizeMapMatcherDescription(t *testing.T) { + var equalsItems = []struct { + description string + actual map[string]bool + matcher *gocrest.Matcher[map[string]bool] + expected string + }{ + {description: "Empty", actual: map[string]bool{"foo": true}, matcher: is.Empty[string, bool, map[string]bool](), expected: "empty value"}, + {description: "HasKey", actual: map[string]bool{"hi": true}, matcher: has.Key[string, bool]("foo"), expected: "map has key 'foo'"}, + {description: "HasKeys", actual: map[string]bool{"hi": true, "bye": false}, matcher: has.AllKeys[string, bool]("hi", "foo"), expected: "map has keys '[hi foo]'"}, + } + for _, test := range equalsItems { + t.Run(test.description, func(innerT *testing.T) { + stubTestingT := new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, test.matcher) + if !strings.Contains(stubTestingT.MockTestOutput, test.expected) { + innerT.Errorf("%s did not fail with expected desc <%s>, got: %s", test.description, test.expected, stubTestingT.MockTestOutput) + } + }) + } +} +func TestArrayContainsMatcherDescription(t *testing.T) { + var equalsItems = []struct { + description string + actual []string + matcher *gocrest.Matcher[[]string] + expected string + }{ + {description: "ArrayContaining", actual: []string{"Foo", "Bar"}, matcher: is.ArrayContaining("Baz", "Bing"), expected: "something that contains and "}, + {description: "ValueContainArrayMatching", actual: []string{"Foo", "Bar"}, matcher: is.ArrayMatching(is.EqualTo("Baz"), is.EqualTo("Bing")), expected: "something that contains > and >"}, + } + for _, test := range equalsItems { + t.Run(test.description, func(innerT *testing.T) { + stubTestingT := new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, test.matcher) + if !strings.Contains(stubTestingT.MockTestOutput, test.expected) { + innerT.Errorf("%s did not fail with expected desc <%s>, got: %s", test.description, test.expected, stubTestingT.MockTestOutput) + } + }) + } +} +func TestStringMatchersDescription(t *testing.T) { + var equalsItems = []struct { + description string + actual string + matcher *gocrest.Matcher[string] + expected string + }{ {description: "MatchesPattern", actual: "blarney stone", matcher: is.MatchForPattern("~123.?.*"), expected: "a value that matches pattern ~123.?.*"}, {description: "MatchesPattern (invalid regex)", actual: "blarney stone", matcher: is.MatchForPattern("+++"), expected: "error parsing regexp: missing argument to repetition operator: `+`"}, {description: "Prefix", actual: "blarney stone", matcher: has.Prefix("123"), expected: "value with prefix 123"}, - {description: "AllOf", actual: "abc", matcher: is.AllOf(is.EqualTo("abc"), is.ValueContaining("e", "f")), expected: "something that contains and "}, - {description: "AnyOf", actual: "abc", matcher: is.AnyOf(is.EqualTo("efg"), is.ValueContaining("e")), expected: "any of (value equal to or something that contains )"}, - {description: "HasKey", actual: map[string]bool{"hi": true}, matcher: has.Key("foo"), expected: "map has key 'foo'"}, - {description: "HasKeys", actual: map[string]bool{"hi": true, "bye": false}, matcher: has.AllKeys("hi", "foo"), expected: "map has keys '[hi foo]'"}, - {description: "LengthOf Composed", actual: "a", matcher: has.Length(is.GreaterThan(2)), expected: "value with length value greater than <2>"}, + {description: "AllOf", actual: "abc", matcher: is.AllOf(is.EqualTo("abc"), is.StringContaining("e", "f")), expected: "something that contains [e f]"}, + {description: "AnyOf", actual: "abc", matcher: is.AnyOf(is.EqualTo("efg"), is.StringContaining("e")), expected: "any of (value equal to or something that contains [e])"}, + {description: "LengthOf Composed", actual: "a", matcher: has.LengthMatching[string, string](is.GreaterThan(2)), expected: "value with length value greater than <2>"}, {description: "EqualToIgnoringWhitespace", actual: "a b c", matcher: is.EqualToIgnoringWhitespace("b c d"), expected: "ignoring whitespace value equal to "}, } for _, test := range equalsItems { @@ -584,24 +826,26 @@ func TestMatcherDescription(t *testing.T) { }) } } + func TestAllOfDescribesOnlyMismatches(testing *testing.T) { stubTestingT := new(StubTestingT) then.AssertThat(stubTestingT, "abc", is.AllOf( is.EqualTo("abc"), - is.ValueContaining("e", "f"), - is.Empty(), + is.StringContaining("e", "f"), + is.Empty[string, string, string](), )) - if !strings.Contains(stubTestingT.MockTestOutput, "Expected: something that contains and and empty value\n") { + if !strings.Contains(stubTestingT.MockTestOutput, "Expected: something that contains [e f] and empty value\n") { testing.Errorf("incorrect description:%s", stubTestingT.MockTestOutput) } } + func TestHasFieldDescribesMismatch(testing *testing.T) { type T struct { F string B string } expected := "X" - then.AssertThat(stubTestingT, new(T), has.FieldNamed(expected)) + then.AssertThat(stubTestingT, new(T), has.FieldNamed[*T](expected)) if !strings.Contains(stubTestingT.MockTestOutput, "struct with field X") && !strings.Contains(stubTestingT.MockTestOutput, "T{F string B string}") { testing.Errorf("incorrect description:%s", stubTestingT.MockTestOutput) @@ -615,7 +859,7 @@ func TestHasFunctionDescribesMismatch(testing *testing.T) { } actual := new(MyType) expected := "X" - then.AssertThat(stubTestingT, actual, has.FunctionNamed(expected)) + then.AssertThat(stubTestingT, actual, has.FunctionNamed[*MyType](expected)) if !strings.Contains(stubTestingT.MockTestOutput, "interface with function X") && !strings.Contains(stubTestingT.MockTestOutput, "MyType{B()F()}") { testing.Errorf("incorrect description:%s", stubTestingT.MockTestOutput) @@ -671,7 +915,7 @@ func TestTypeName(t *testing.T) { for _, tt := range tests { t.Run(tt.expected, func(t *testing.T) { stubTestingT := new(StubTestingT) - then.AssertThat(stubTestingT, tt.actual, has.TypeName(tt.expected)) + then.AssertThat(stubTestingT, tt.actual, has.TypeName[any](tt.expected)) then.AssertThat(t, stubTestingT.HasFailed(), is.EqualTo(tt.shouldFail).Reason(stubTestingT.MockTestOutput)) }) @@ -681,44 +925,53 @@ func TestTypeName(t *testing.T) { func TestNilArrayInterface(t *testing.T) { actual := nilResponse() - then.AssertThat(t, actual, is.Nil()) + then.AssertThat(t, actual, is.Nil[[]any]()) } -func nilResponse() []interface{} { +func nilResponse() []any { return nil } -func TestEveryElement(t *testing.T) { +func TestEveryStringElement(t *testing.T) { tests := []struct { - actual interface{} - expected []*gocrest.Matcher + actual []string + expected []*gocrest.Matcher[string] shouldFail bool }{ { actual: []string{"test1", "test2"}, - expected: []*gocrest.Matcher{is.EqualTo("test1"), is.EqualTo("test2")}, + expected: []*gocrest.Matcher[string]{is.EqualTo("test1"), is.EqualTo("test2")}, shouldFail: false, }, + } + for _, test := range tests { + stubTestingT := new(StubTestingT) + then.AssertThat(stubTestingT, test.actual, has.EveryElement(test.expected...)) + + then.AssertThat(t, stubTestingT.HasFailed(), is.EqualTo(test.shouldFail).Reason(stubTestingT.MockTestOutput)) + } +} +func TestEveryIntElement(t *testing.T) { + tests := []struct { + actual []int + expected []*gocrest.Matcher[int] + shouldFail bool + }{ { actual: []int{1, 2}, - expected: []*gocrest.Matcher{is.EqualTo(1), is.EqualTo(2)}, + expected: []*gocrest.Matcher[int]{is.EqualTo(1), is.EqualTo(2)}, shouldFail: false, }, - { - actual: []string{"test1", "test2"}, - expected: []*gocrest.Matcher{is.EqualTo("test1"), is.EqualTo("nottest")}, - shouldFail: true, - }, { actual: []int{1, 2}, - expected: []*gocrest.Matcher{ + expected: []*gocrest.Matcher[int]{ is.EqualTo(1), }, shouldFail: true, }, { actual: []int{1}, - expected: []*gocrest.Matcher{ + expected: []*gocrest.Matcher[int]{ is.EqualTo(1), is.EqualTo(2), }, @@ -732,42 +985,18 @@ func TestEveryElement(t *testing.T) { then.AssertThat(t, stubTestingT.HasFailed(), is.EqualTo(test.shouldFail).Reason(stubTestingT.MockTestOutput)) } } -func TestEveryElementPanic(t *testing.T) { - tests := []struct { - actual string - expected []*gocrest.Matcher - }{ - { - actual: "not a slice", - expected: []*gocrest.Matcher{ - is.Empty(), - }, - }, - } - - defer func() { - recover := recover() - then.AssertThat(t, recover, is.Not(is.Nil())) - }() - - for _, test := range tests { - stubTestingT := new(StubTestingT) - - then.AssertThat(stubTestingT, test.actual, has.EveryElement(test.expected...)) - } -} func TestStructValues(t *testing.T) { tests := []struct { - actual interface{} - expected has.StructMatchers + actual any + expected has.StructMatchers[string] shouldFail bool }{ { actual: struct { Id string }{Id: "Id"}, - expected: has.StructMatchers{ + expected: has.StructMatchers[string]{ "Id": has.Prefix("Id"), }, shouldFail: false, @@ -777,7 +1006,7 @@ func TestStructValues(t *testing.T) { Id string Id2 string }{Id: "Id", Id2: "Id2"}, - expected: has.StructMatchers{ + expected: has.StructMatchers[string]{ "Id": has.Prefix("Id"), }, shouldFail: false, @@ -786,8 +1015,8 @@ func TestStructValues(t *testing.T) { actual: struct { Id string }{}, - expected: has.StructMatchers{ - "Id": is.Empty(), + expected: has.StructMatchers[string]{ + "Id": is.Empty[string, string, string](), }, shouldFail: false, }, @@ -795,7 +1024,7 @@ func TestStructValues(t *testing.T) { actual: struct { Id string }{}, - expected: has.StructMatchers{ + expected: has.StructMatchers[string]{ "Id": is.EqualTo("something"), }, shouldFail: true, @@ -805,7 +1034,7 @@ func TestStructValues(t *testing.T) { Id string Id2 string }{}, - expected: has.StructMatchers{ + expected: has.StructMatchers[string]{ "Id2": is.EqualTo("something"), }, shouldFail: true, @@ -815,7 +1044,7 @@ func TestStructValues(t *testing.T) { Id string Id2 string }{}, - expected: has.StructMatchers{ + expected: has.StructMatchers[string]{ "Id": is.EqualTo("Id"), "Id2": is.EqualTo("something"), }, @@ -824,7 +1053,7 @@ func TestStructValues(t *testing.T) { } for _, test := range tests { stubTestingT := new(StubTestingT) - then.AssertThat(stubTestingT, test.actual, has.StructWithValues(test.expected)) + then.AssertThat(stubTestingT, test.actual, has.StructWithValues[any](test.expected)) then.AssertThat(t, stubTestingT.HasFailed(), is.EqualTo(test.shouldFail).Reason(stubTestingT.MockTestOutput)) } @@ -832,53 +1061,18 @@ func TestStructValues(t *testing.T) { func TestStructValuesPanicsWithStringActual(t *testing.T) { actual := "not a struct" - expected := has.StructMatchers{ - "Id": is.Empty(), + expected := has.StructMatchers[string]{ + "Id": is.Empty[string, string, string](), } defer func() { recover := recover() - then.AssertThat(t, recover, is.Not(is.Nil())) + then.AssertThat(t, recover, is.Not(is.Nil[any]())) }() - then.AssertThat(stubTestingT, actual, has.StructWithValues(expected)) -} - -func TestStructValuesPanic(t *testing.T) { - tests := []struct { - actual interface{} - expected has.StructMatchers - }{ - { - actual: struct { - Id string - }{}, - expected: has.StructMatchers{ - "Id2": is.Empty(), - }, - }, - { - actual: struct { - id string - }{}, - expected: has.StructMatchers{ - "id": is.Empty(), - }, - }, - } - - defer func() { - recover := recover() - then.AssertThat(t, recover, is.Not(is.Nil())) - }() - - for _, test := range tests { - stubTestingT := new(StubTestingT) - - then.AssertThat(stubTestingT, test.actual, has.StructWithValues(test.expected)) - } + then.AssertThat(stubTestingT, actual, has.StructWithValues[string](expected)) } func TestConformsToStringer(t *testing.T) { - then.AssertThat(t, is.Nil().String(), is.EqualTo("value that is ")) + then.AssertThat(t, is.Nil[any]().String(), is.EqualTo("value that is ")) } type DelayedReader struct { @@ -900,6 +1094,7 @@ func TestEventuallyWithDelayedReader(t *testing.T) { then.AssertThat(eventually, by.Reading(slowReader, 1024), is.EqualTo([]byte("abcdefghijklmnopqrstuv"))) }) } + func TestEventuallyChannels(t *testing.T) { channel := firstTestChannel() then.Eventually(t, time.Second*5, time.Second, func(eventually gocrest.TestingT) { @@ -917,10 +1112,11 @@ func TestEventuallyChannelsShouldFail(t *testing.T) { }) then.AssertThat(t, stubbedTesting.failed, is.EqualTo(true)) then.AssertThat(t, stubbedTesting.MockTestOutput, is.AllOf( - is.ValueContaining("This is going to fail"), - is.Not(is.ValueContaining("should not fail")), + is.StringContaining("This is going to fail"), + is.Not(is.StringContaining("should not fail")), )) } + func TestEventuallyChannelInterface(t *testing.T) { type MyType struct { F string @@ -939,13 +1135,22 @@ func TestEventuallyChannelInterface(t *testing.T) { } }() then.WithinFiveSeconds(t, func(eventually gocrest.TestingT) { - then.AssertThat(eventually, by.Channelling(channel), has.StructWithValues(has.StructMatchers{ + then.AssertThat(eventually, by.Channelling(channel), has.StructWithValues[*MyType](has.StructMatchers[string]{ "F": is.EqualTo("hi - 3"), "B": is.EqualTo("bye - 3"), })) }) } +func TestCallingFunctionEventually(t *testing.T) { + function := func(a string) string { + time.Sleep(time.Second) + return a + } + then.WithinFiveSeconds(t, func(eventually gocrest.TestingT) { + then.AssertThat(eventually, by.Calling[string, string](function, "hi"), is.EqualTo("hi")) + }) +} func firstTestChannel() chan int { channel := make(chan int, 1) go func() { diff --git a/then/assertthat.go b/then/assertthat.go index f40ddd1..2b90499 100644 --- a/then/assertthat.go +++ b/then/assertthat.go @@ -6,7 +6,7 @@ import ( ) // AssertThat calls a given matcher and fails the test with a message if the matcher doesn't match. -func AssertThat(t gocrest.TestingT, actual interface{}, m *gocrest.Matcher) { +func AssertThat[A any](t gocrest.TestingT, actual A, m *gocrest.Matcher[A]) { t.Helper() matches := m.Matches(actual) if !matches { @@ -19,7 +19,7 @@ func AssertThat(t gocrest.TestingT, actual interface{}, m *gocrest.Matcher) { } } -func actualAsString(matcher *gocrest.Matcher, actual interface{}) string { +func actualAsString[A any](matcher *gocrest.Matcher[A], actual A) string { if matcher.Actual != "" { return matcher.Actual } diff --git a/then/eventually.go b/then/eventually.go index 7331b98..99c98bb 100644 --- a/then/eventually.go +++ b/then/eventually.go @@ -8,16 +8,20 @@ import ( "time" ) +// FailureLog is a type that holds temporary test data whilst Eventually runs type FailureLog struct { TestOutput string failed bool } + +// RecordingTestingT is a testingT interface that holds temporary test data whilst Eventually runs. Only used with Eventually type RecordingTestingT struct { sync.Mutex gocrest.TestingT failures []FailureLog } +// Errorf appends an error message into the failure buffer. Only used with Eventually func (t *RecordingTestingT) Errorf(format string, args ...interface{}) { t.Lock() defer t.Unlock() @@ -26,12 +30,21 @@ func (t *RecordingTestingT) Errorf(format string, args ...interface{}) { true, }) } + +// Fail calls Errorf with a generic failure message in case the test is failed from inside the value under test. +// Only used with Eventually func (t *RecordingTestingT) Fail() { t.Errorf("Unknown call to Fail") } + +// FailNow calls Errorf with a generic failure message in case the test is failed from inside the value under test. +// Only used with Eventually func (t *RecordingTestingT) FailNow() { t.Errorf("Unknown call to FailNow") } + +// FailedTestOutputs retrieves the list of recorded testing.T failures from the assertions passed in for Eventually evaluation. +// Only used with Eventually func (t *RecordingTestingT) FailedTestOutputs() []string { t.Lock() defer t.Unlock() @@ -41,6 +54,9 @@ func (t *RecordingTestingT) FailedTestOutputs() []string { } return logs } + +// Failing determines whether any of the Eventually assertions are still failing. +// Only used with Eventually func (t *RecordingTestingT) Failing() bool { t.Lock() defer t.Unlock() @@ -52,16 +68,23 @@ func (t *RecordingTestingT) Failing() bool { return false } +// Latest is used internally by Eventually to record the last recorded test output from assertions passed into it +// Only used with Eventually type Latest struct { sync.Mutex latestValue RecordingTestingT } +// Get is used internally by Eventually to Get the last recorded test output from assertions passed into it +// Only used with Eventually func (l *Latest) Get() RecordingTestingT { l.Lock() defer l.Unlock() return l.latestValue } + +// Merge is used internally by Eventually to Merge the latest recorded test output from assertions passed into it with the last one +// Only used with Eventually func (l *Latest) Merge(updated RecordingTestingT) RecordingTestingT { l.Lock() defer l.Unlock() @@ -84,7 +107,20 @@ func (l *Latest) Merge(updated RecordingTestingT) RecordingTestingT { } return merged } -func Eventually(t gocrest.TestingT, waitFor time.Duration, tick time.Duration, assertions func(eventually gocrest.TestingT)) { + +// Eventually retries a set of assertions passed into it from a function. +// waitFor is the amount of time to wait for the assertions to pass. +// tick is the amount of time between running the assertions +// assertions are the function containing the assertions. +// e.g: +// ``` +// +// then.Eventually(t, time.Second*5, time.Second, func(eventually gocrest.TestingT) { +// then.AssertThat(eventually, by.Channelling(channel), is.EqualTo(3).Reason("should not fail")) +// }) +// +// ``` +func Eventually(t gocrest.TestingT, waitFor, tick time.Duration, assertions func(eventually gocrest.TestingT)) { t.Helper() channel := make(chan RecordingTestingT, 1) @@ -101,7 +137,7 @@ func Eventually(t gocrest.TestingT, waitFor time.Duration, tick time.Duration, a select { case <-timer.C: latestRecordingT := latestValue.Get() - t.Errorf("Eventually Failed: \n" + strings.Join(latestRecordingT.FailedTestOutputs(), "\n")) + t.Errorf(fmt.Sprintf("Eventually Failed after %s: \n", waitFor) + strings.Join(latestRecordingT.FailedTestOutputs(), "\n")) return case <-tick: tick = nil @@ -123,9 +159,13 @@ func Eventually(t gocrest.TestingT, waitFor time.Duration, tick time.Duration, a } } + +// WithinTenSeconds is a shortcut for a ten-second Eventually call with one second tick func WithinTenSeconds(t gocrest.TestingT, assertions func(eventually gocrest.TestingT)) { Eventually(t, time.Duration(10)*time.Second, time.Duration(1)*time.Second, assertions) } + +// WithinFiveSeconds is a shortcut for a five-second Eventually call with one second tick func WithinFiveSeconds(t gocrest.TestingT, assertions func(eventually gocrest.TestingT)) { Eventually(t, time.Duration(10)*time.Second, time.Duration(1)*time.Second, assertions) }