Repository: stretchr/testify
Pull request #1022 (2020-11-09 01:33:23)

Add ErrorContains
## Summary
It's common practice to wrap errors, so that you do not have a transitive dependency on the error format of a third party library, but also so that you are able to exactly match you are testing a specific error case correctly.

This change adds an `ErrorContains()` method to check that both an error is reurned, and it contains a provided string.

## Changes
Added `ErrorContains()` and `ErrorContainsf()`.

## Motivation

When I wrap errors returned from third party modules:

```go
func method(f foo, b bar) error {
        if err := f(); err != nil {
                return fmt.Errorf("foo failed: %w", err)
        }
        if err := b(); err != nil {
                return fmt.Errorf("bar failed: %w", err)
        }
        return nil
}
```

This then forces me to write:

```go
f := func() error { return assert.AnError }
b := func() error { return nil }
err := method(f, b)
require.Error(t, err)
assert.Contains(t, err.Error(), "foo failed")

f = func() error { return nil }
b = func() error { return assert.AnError }
err = method(f, b)
require.Error(t, err)
assert.Contains(t, err.Error(), "bar failed")
```
This change removes the `require` and `Error()` unpacking:

```go
f := func() error { return assert.AnError }
b := func() error { return nil }
err := method(f, b)
assert.ErrorContains(t, err, "foo failed")

f = func() error { return nil }
b = func() error { return assert.AnError }
err = method(f, b)
assert.ErrorContains(t, err, "bar failed")
```

## Related issues




Patch:
diff --git a/README.md b/README.md
index b0ea7b8..c78250e 100644
--- a/README.md
+++ b/README.md
@@ -323,7 +323,7 @@ To update Testify to the latest version, use `go get -u github.com/stretchr/test
 Supported go versions
 ==================
 
-We support the two major Go versions, which are 1.13 and 1.14 at the moment.
+We support the three major Go versions, which are 1.13, 1.14 and 1.15 at the moment.
 
 ------
 
diff --git a/assert/assertion_compare.go b/assert/assertion_compare.go
index 41649d2..aff1c4b 100644
--- a/assert/assertion_compare.go
+++ b/assert/assertion_compare.go
@@ -310,6 +310,9 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
 //    assert.Greater(t, float64(2), float64(1))
 //    assert.Greater(t, "b", "a")
 func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
 	return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs)
 }
 
@@ -320,6 +323,9 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface
 //    assert.GreaterOrEqual(t, "b", "a")
 //    assert.GreaterOrEqual(t, "b", "b")
 func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
 	return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs)
 }
 
@@ -329,6 +335,9 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in
 //    assert.Less(t, float64(1), float64(2))
 //    assert.Less(t, "a", "b")
 func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
 	return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs)
 }
 
@@ -339,6 +348,9 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{})
 //    assert.LessOrEqual(t, "a", "b")
 //    assert.LessOrEqual(t, "b", "b")
 func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
 	return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs)
 }
 
@@ -347,6 +359,9 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter
 //    assert.Positive(t, 1)
 //    assert.Positive(t, 1.23)
 func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
 	zero := reflect.Zero(reflect.TypeOf(e))
 	return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs)
 }
@@ -356,6 +371,9 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
 //    assert.Negative(t, -1)
 //    assert.Negative(t, -1.23)
 func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
 	zero := reflect.Zero(reflect.TypeOf(e))
 	return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs)
 }
diff --git a/assert/assertion_format.go b/assert/assertion_format.go
index 4dfd122..27e2420 100644
--- a/assert/assertion_format.go
+++ b/assert/assertion_format.go
@@ -123,6 +123,18 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int
 	return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...)
 }
 
+// ErrorContainsf asserts that a function returned an error (i.e. not `nil`)
+// and that the error contains the specified substring.
+//
+//   actualObj, err := SomeFunction()
+//   assert.ErrorContainsf(t, err,  expectedErrorSubString, "error message %s", "formatted")
+func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
+	return ErrorContains(t, theError, contains, append([]interface{}{msg}, args...)...)
+}
+
 // ErrorIsf asserts that at least one of the errors in err's chain matches target.
 // This is a wrapper for errors.Is.
 func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool {
diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go
index 25337a6..d9ea368 100644
--- a/assert/assertion_forward.go
+++ b/assert/assertion_forward.go
@@ -222,6 +222,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ..
 	return ErrorAsf(a.t, err, target, msg, args...)
 }
 
+// ErrorContains asserts that a function returned an error (i.e. not `nil`)
+// and that the error contains the specified substring.
+//
+//   actualObj, err := SomeFunction()
+//   a.ErrorContains(err,  expectedErrorSubString)
+func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) bool {
+	if h, ok := a.t.(tHelper); ok {
+		h.Helper()
+	}
+	return ErrorContains(a.t, theError, contains, msgAndArgs...)
+}
+
+// ErrorContainsf asserts that a function returned an error (i.e. not `nil`)
+// and that the error contains the specified substring.
+//
+//   actualObj, err := SomeFunction()
+//   a.ErrorContainsf(err,  expectedErrorSubString, "error message %s", "formatted")
+func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) bool {
+	if h, ok := a.t.(tHelper); ok {
+		h.Helper()
+	}
+	return ErrorContainsf(a.t, theError, contains, msg, args...)
+}
+
 // ErrorIs asserts that at least one of the errors in err's chain matches target.
 // This is a wrapper for errors.Is.
 func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool {
diff --git a/assert/assertions.go b/assert/assertions.go
index bcac440..00d97a5 100644
--- a/assert/assertions.go
+++ b/assert/assertions.go
@@ -1375,6 +1375,27 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte
 	return true
 }
 
+// ErrorContains asserts that a function returned an error (i.e. not `nil`)
+// and that the error contains the specified substring.
+//
+//   actualObj, err := SomeFunction()
+//   assert.ErrorContains(t, err,  expectedErrorSubString)
+func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
+	if !Error(t, theError, msgAndArgs...) {
+		return false
+	}
+
+	actual := theError.Error()
+	if !strings.Contains(actual, contains) {
+		return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...)
+	}
+
+	return true
+}
+
 // matchRegexp return true if a specified regexp matches a string.
 func matchRegexp(rx interface{}, str interface{}) bool {
 
diff --git a/mock/mock.go b/mock/mock.go
index c6df448..e2e6a2d 100644
--- a/mock/mock.go
+++ b/mock/mock.go
@@ -297,25 +297,52 @@ func (m *Mock) findExpectedCall(method string, arguments ...interface{}) (int, *
 	return -1, expectedCall
 }
 
+type matchCandidate struct {
+	call      *Call
+	mismatch  string
+	diffCount int
+}
+
+func (c matchCandidate) isBetterMatchThan(other matchCandidate) bool {
+	if c.call == nil {
+		return false
+	}
+	if other.call == nil {
+		return true
+	}
+
+	if c.diffCount > other.diffCount {
+		return false
+	}
+	if c.diffCount < other.diffCount {
+		return true
+	}
+
+	if c.call.Repeatability > 0 && other.call.Repeatability <= 0 {
+		return true
+	}
+	return false
+}
+
 func (m *Mock) findClosestCall(method string, arguments ...interface{}) (*Call, string) {
-	var diffCount int
-	var closestCall *Call
-	var err string
+	var bestMatch matchCandidate
 
 	for _, call := range m.expectedCalls() {
 		if call.Method == method {
 
 			errInfo, tempDiffCount := call.Arguments.Diff(arguments)
-			if tempDiffCount < diffCount || diffCount == 0 {
-				diffCount = tempDiffCount
-				closestCall = call
-				err = errInfo
+			tempCandidate := matchCandidate{
+				call:      call,
+				mismatch:  errInfo,
+				diffCount: tempDiffCount,
+			}
+			if tempCandidate.isBetterMatchThan(bestMatch) {
+				bestMatch = tempCandidate
 			}
-
 		}
 	}
 
-	return closestCall, err
+	return bestMatch.call, bestMatch.mismatch
 }
 
 func callString(method string, arguments Arguments, includeArgumentValues bool) string {
diff --git a/require/require.go b/require/require.go
index 51820df..59c4827 100644
--- a/require/require.go
+++ b/require/require.go
@@ -280,6 +280,36 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int
 	t.FailNow()
 }
 
+// ErrorContains asserts that a function returned an error (i.e. not `nil`)
+// and that the error contains the specified substring.
+//
+//   actualObj, err := SomeFunction()
+//   assert.ErrorContains(t, err,  expectedErrorSubString)
+func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
+	if assert.ErrorContains(t, theError, contains, msgAndArgs...) {
+		return
+	}
+	t.FailNow()
+}
+
+// ErrorContainsf asserts that a function returned an error (i.e. not `nil`)
+// and that the error contains the specified substring.
+//
+//   actualObj, err := SomeFunction()
+//   assert.ErrorContainsf(t, err,  expectedErrorSubString, "error message %s", "formatted")
+func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
+	if assert.ErrorContainsf(t, theError, contains, msg, args...) {
+		return
+	}
+	t.FailNow()
+}
+
 // ErrorIs asserts that at least one of the errors in err's chain matches target.
 // This is a wrapper for errors.Is.
 func ErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) {
diff --git a/require/require_forward.go b/require/require_forward.go
index ed54a9d..5bb07c8 100644
--- a/require/require_forward.go
+++ b/require/require_forward.go
@@ -223,6 +223,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ..
 	ErrorAsf(a.t, err, target, msg, args...)
 }
 
+// ErrorContains asserts that a function returned an error (i.e. not `nil`)
+// and that the error contains the specified substring.
+//
+//   actualObj, err := SomeFunction()
+//   a.ErrorContains(err,  expectedErrorSubString)
+func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) {
+	if h, ok := a.t.(tHelper); ok {
+		h.Helper()
+	}
+	ErrorContains(a.t, theError, contains, msgAndArgs...)
+}
+
+// ErrorContainsf asserts that a function returned an error (i.e. not `nil`)
+// and that the error contains the specified substring.
+//
+//   actualObj, err := SomeFunction()
+//   a.ErrorContainsf(err,  expectedErrorSubString, "error message %s", "formatted")
+func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) {
+	if h, ok := a.t.(tHelper); ok {
+		h.Helper()
+	}
+	ErrorContainsf(a.t, theError, contains, msg, args...)
+}
+
 // ErrorIs asserts that at least one of the errors in err's chain matches target.
 // This is a wrapper for errors.Is.
 func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) {


Test patch:
diff --git a/assert/assertion_compare_test.go b/assert/assertion_compare_test.go
index b4631a4..da5a330 100644
--- a/assert/assertion_compare_test.go
+++ b/assert/assertion_compare_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"reflect"
+	"runtime"
 	"testing"
 )
 
@@ -82,7 +83,8 @@ func TestCompare(t *testing.T) {
 }
 
 type outputT struct {
-	buf *bytes.Buffer
+	buf     *bytes.Buffer
+	helpers map[string]struct{}
 }
 
 // Implements TestingT
@@ -91,6 +93,27 @@ func (t *outputT) Errorf(format string, args ...interface{}) {
 	t.buf.WriteString(s)
 }
 
+func (t *outputT) Helper() {
+	if t.helpers == nil {
+		t.helpers = make(map[string]struct{})
+	}
+	t.helpers[callerName(1)] = struct{}{}
+}
+
+// callerName gives the function name (qualified with a package path)
+// for the caller after skip frames (where 0 means the current function).
+func callerName(skip int) string {
+	// Make room for the skip PC.
+	var pc [1]uintptr
+	n := runtime.Callers(skip+2, pc[:]) // skip + runtime.Callers + callerName
+	if n == 0 {
+		panic("testing: zero callers found")
+	}
+	frames := runtime.CallersFrames(pc[:n])
+	frame, _ := frames.Next()
+	return frame.Function
+}
+
 func TestGreater(t *testing.T) {
 	mockT := new(testing.T)
 
@@ -128,6 +151,7 @@ func TestGreater(t *testing.T) {
 		out := &outputT{buf: bytes.NewBuffer(nil)}
 		False(t, Greater(out, currCase.less, currCase.greater))
 		Contains(t, string(out.buf.Bytes()), currCase.msg)
+		Contains(t, out.helpers, "github.com/stretchr/testify/assert.Greater")
 	}
 }
 
@@ -168,6 +192,7 @@ func TestGreaterOrEqual(t *testing.T) {
 		out := &outputT{buf: bytes.NewBuffer(nil)}
 		False(t, GreaterOrEqual(out, currCase.less, currCase.greater))
 		Contains(t, string(out.buf.Bytes()), currCase.msg)
+		Contains(t, out.helpers, "github.com/stretchr/testify/assert.GreaterOrEqual")
 	}
 }
 
@@ -208,6 +233,7 @@ func TestLess(t *testing.T) {
 		out := &outputT{buf: bytes.NewBuffer(nil)}
 		False(t, Less(out, currCase.greater, currCase.less))
 		Contains(t, string(out.buf.Bytes()), currCase.msg)
+		Contains(t, out.helpers, "github.com/stretchr/testify/assert.Less")
 	}
 }
 
@@ -248,6 +274,7 @@ func TestLessOrEqual(t *testing.T) {
 		out := &outputT{buf: bytes.NewBuffer(nil)}
 		False(t, LessOrEqual(out, currCase.greater, currCase.less))
 		Contains(t, string(out.buf.Bytes()), currCase.msg)
+		Contains(t, out.helpers, "github.com/stretchr/testify/assert.LessOrEqual")
 	}
 }
 
@@ -286,6 +313,7 @@ func TestPositive(t *testing.T) {
 		out := &outputT{buf: bytes.NewBuffer(nil)}
 		False(t, Positive(out, currCase.e))
 		Contains(t, string(out.buf.Bytes()), currCase.msg)
+		Contains(t, out.helpers, "github.com/stretchr/testify/assert.Positive")
 	}
 }
 
@@ -324,6 +352,7 @@ func TestNegative(t *testing.T) {
 		out := &outputT{buf: bytes.NewBuffer(nil)}
 		False(t, Negative(out, currCase.e))
 		Contains(t, string(out.buf.Bytes()), currCase.msg)
+		Contains(t, out.helpers, "github.com/stretchr/testify/assert.Negative")
 	}
 }
 
diff --git a/assert/assertions_test.go b/assert/assertions_test.go
index f32362b..3bd418d 100644
--- a/assert/assertions_test.go
+++ b/assert/assertions_test.go
@@ -1079,6 +1079,24 @@ func TestEqualError(t *testing.T) {
 		"EqualError should return true")
 }
 
+func TestErrorContains(t *testing.T) {
+	mockT := new(testing.T)
+
+	// start with a nil error
+	var err error
+	False(t, ErrorContains(mockT, err, ""),
+		"ErrorContains should return false for nil arg")
+
+	// now set an error
+	err = errors.New("some error: another error")
+	False(t, ErrorContains(mockT, err, "bad error"),
+		"ErrorContains should return false for different error string")
+	True(t, ErrorContains(mockT, err, "some error"),
+		"ErrorContains should return true")
+	True(t, ErrorContains(mockT, err, "another error"),
+		"ErrorContains should return true")
+}
+
 func Test_isEmpty(t *testing.T) {
 
 	chWithValue := make(chan struct{}, 1)
diff --git a/assert/forward_assertions_test.go b/assert/forward_assertions_test.go
index a8240d7..496d3c0 100644
--- a/assert/forward_assertions_test.go
+++ b/assert/forward_assertions_test.go
@@ -315,6 +315,25 @@ func TestErrorWrapper(t *testing.T) {
 
 }
 
+func TestErrorContainsWrapper(t *testing.T) {
+	assert := New(t)
+	mockAssert := New(new(testing.T))
+
+	// start with a nil error
+	var err error
+	assert.False(mockAssert.ErrorContains(err, ""),
+		"ErrorContains should return false for nil arg")
+
+	// now set an error
+	err = errors.New("some error: another error")
+	assert.False(mockAssert.ErrorContains(err, "different error"),
+		"ErrorContains should return false for different error string")
+	assert.True(mockAssert.ErrorContains(err, "some error"),
+		"ErrorContains should return true")
+	assert.True(mockAssert.ErrorContains(err, "another error"),
+		"ErrorContains should return true")
+}
+
 func TestEqualErrorWrapper(t *testing.T) {
 	assert := New(t)
 	mockAssert := New(new(testing.T))
diff --git a/mock/mock_test.go b/mock/mock_test.go
index cb49541..2b6e704 100644
--- a/mock/mock_test.go
+++ b/mock/mock_test.go
@@ -1557,7 +1557,7 @@ func TestArgumentMatcherToPrintMismatch(t *testing.T) {
 func TestClosestCallMismatchedArgumentInformationShowsTheClosest(t *testing.T) {
 	defer func() {
 		if r := recover(); r != nil {
-			matchingExp := regexp.MustCompile(unexpectedCallRegex(`TheExampleMethod(int,int,int)`, `0: 1\s+1: 1\s+2: 2`, `0: 1\s+1: 1\s+2: 1`, `0: PASS:  \(int=1\) == \(int=1\)\s+1: PASS:  \(int=1\) == \(int=1\)\s+2: FAIL:  \(int=2\) != \(int=1\)`))
+			matchingExp := regexp.MustCompile(unexpectedCallRegex(`TheExampleMethod(int,int,int)`, `0: 1\s+1: 1\s+2: 2`, `0: 1\s+1: 1\s+2: 1`, `Diff: 0: PASS:  \(int=1\) == \(int=1\)\s+1: PASS:  \(int=1\) == \(int=1\)\s+2: FAIL:  \(int=2\) != \(int=1\)`))
 			assert.Regexp(t, matchingExp, r)
 		}
 	}()
@@ -1569,10 +1569,46 @@ func TestClosestCallMismatchedArgumentInformationShowsTheClosest(t *testing.T) {
 	m.TheExampleMethod(1, 1, 2)
 }
 
+func TestClosestCallFavorsFirstMock(t *testing.T) {
+	defer func() {
+		if r := recover(); r != nil {
+			diffRegExp := `Difference found in argument 0:\s+--- Expected\s+\+\+\+ Actual\s+@@ -2,4 \+2,4 @@\s+\(bool\) true,\s+- \(bool\) true,\s+- \(bool\) true\s+\+ \(bool\) false,\s+\+ \(bool\) false\s+}\s+`
+			matchingExp := regexp.MustCompile(unexpectedCallRegex(`TheExampleMethod7([]bool)`, `0: \[\]bool{true, false, false}`, `0: \[\]bool{true, true, true}`, diffRegExp))
+			assert.Regexp(t, matchingExp, r)
+		}
+	}()
+
+	m := new(TestExampleImplementation)
+	m.On("TheExampleMethod7", []bool{true, true, true}).Return(nil).Once()
+	m.On("TheExampleMethod7", []bool{false, false, false}).Return(nil).Once()
+
+	m.TheExampleMethod7([]bool{true, false, false})
+}
+
+func TestClosestCallUsesRepeatabilityToFindClosest(t *testing.T) {
+	defer func() {
+		if r := recover(); r != nil {
+			diffRegExp := `Difference found in argument 0:\s+--- Expected\s+\+\+\+ Actual\s+@@ -1,4 \+1,4 @@\s+\(\[\]bool\) \(len=3\) {\s+- \(bool\) false,\s+- \(bool\) false,\s+\+ \(bool\) true,\s+\+ \(bool\) true,\s+\(bool\) false\s+`
+			matchingExp := regexp.MustCompile(unexpectedCallRegex(`TheExampleMethod7([]bool)`, `0: \[\]bool{true, true, false}`, `0: \[\]bool{false, false, false}`, diffRegExp))
+			assert.Regexp(t, matchingExp, r)
+		}
+	}()
+
+	m := new(TestExampleImplementation)
+	m.On("TheExampleMethod7", []bool{true, true, true}).Return(nil).Once()
+	m.On("TheExampleMethod7", []bool{false, false, false}).Return(nil).Once()
+
+	m.TheExampleMethod7([]bool{true, true, true})
+
+	// Since the first mocked call has already been used, it now has no repeatability,
+	// thus the second mock should be shown as the closest match
+	m.TheExampleMethod7([]bool{true, true, false})
+}
+
 func TestClosestCallMismatchedArgumentValueInformation(t *testing.T) {
 	defer func() {
 		if r := recover(); r != nil {
-			matchingExp := regexp.MustCompile(unexpectedCallRegex(`GetTime(int)`, "0: 1", "0: 999", `0: FAIL:  \(int=1\) != \(int=999\)`))
+			matchingExp := regexp.MustCompile(unexpectedCallRegex(`GetTime(int)`, "0: 1", "0: 999", `Diff: 0: FAIL:  \(int=1\) != \(int=999\)`))
 			assert.Regexp(t, matchingExp, r)
 		}
 	}()
@@ -1583,9 +1619,33 @@ func TestClosestCallMismatchedArgumentValueInformation(t *testing.T) {
 	_ = m.GetTime(1)
 }
 
+func Test_isBetterMatchThanReturnsFalseIfCandidateCallIsNil(t *testing.T) {
+	assert.False(t, matchCandidate{}.isBetterMatchThan(matchCandidate{}))
+}
+
+func Test_isBetterMatchThanReturnsTrueIfOtherCandidateCallIsNil(t *testing.T) {
+	assert.True(t, matchCandidate{call: &Call{}}.isBetterMatchThan(matchCandidate{}))
+}
+
+func Test_isBetterMatchThanReturnsFalseIfDiffCountIsGreaterThanOther(t *testing.T) {
+	assert.False(t, matchCandidate{call: &Call{}, diffCount: 2}.isBetterMatchThan(matchCandidate{call: &Call{}, diffCount: 1}))
+}
+
+func Test_isBetterMatchThanReturnsTrueIfDiffCountIsLessThanOther(t *testing.T) {
+	assert.True(t, matchCandidate{call: &Call{}, diffCount: 1}.isBetterMatchThan(matchCandidate{call: &Call{}, diffCount: 2}))
+}
+
+func Test_isBetterMatchThanReturnsTrueIfRepeatabilityIsGreaterThanOther(t *testing.T) {
+	assert.True(t, matchCandidate{call: &Call{Repeatability: 1}, diffCount: 1}.isBetterMatchThan(matchCandidate{call: &Call{Repeatability: -1}, diffCount: 1}))
+}
+
+func Test_isBetterMatchThanReturnsFalseIfRepeatabilityIsLessThanOrEqualToOther(t *testing.T) {
+	assert.False(t, matchCandidate{call: &Call{Repeatability: 1}, diffCount: 1}.isBetterMatchThan(matchCandidate{call: &Call{Repeatability: 1}, diffCount: 1}))
+}
+
 func unexpectedCallRegex(method, calledArg, expectedArg, diff string) string {
 	rMethod := regexp.QuoteMeta(method)
-	return fmt.Sprintf(`\s+mock: Unexpected Method Call\s+-*\s+%s\s+%s\s+The closest call I have is:\s+%s\s+%s\s+Diff: %s`,
+	return fmt.Sprintf(`\s+mock: Unexpected Method Call\s+-*\s+%s\s+%s\s+The closest call I have is:\s+%s\s+%s\s+%s`,
 		rMethod, calledArg, rMethod, expectedArg, diff)
 }
 
diff --git a/require/forward_requirements_test.go b/require/forward_requirements_test.go
index 1ec65fa..8fbcc15 100644
--- a/require/forward_requirements_test.go
+++ b/require/forward_requirements_test.go
@@ -196,6 +196,18 @@ func TestErrorWrapper(t *testing.T) {
 	}
 }
 
+func TestErrorContainsWrapper(t *testing.T) {
+	require := New(t)
+	require.ErrorContains(errors.New("some error: another error"), "some error")
+
+	mockT := new(MockT)
+	mockRequire := New(mockT)
+	mockRequire.ErrorContains(errors.New("some error: another error"), "different error")
+	if !mockT.Failed {
+		t.Error("Check should fail")
+	}
+}
+
 func TestEqualErrorWrapper(t *testing.T) {
 	require := New(t)
 	require.EqualError(errors.New("some error"), "some error")
diff --git a/require/requirements_test.go b/require/requirements_test.go
index c43e989..febf0c1 100644
--- a/require/requirements_test.go
+++ b/require/requirements_test.go
@@ -208,6 +208,17 @@ func TestError(t *testing.T) {
 	}
 }
 
+func TestErrorContains(t *testing.T) {
+
+	ErrorContains(t, errors.New("some error: another error"), "some error")
+
+	mockT := new(MockT)
+	ErrorContains(mockT, errors.New("some error"), "different error")
+	if !mockT.Failed {
+		t.Error("Check should fail")
+	}
+}
+
 func TestEqualError(t *testing.T) {
 
 	EqualError(t, errors.New("some error"), "some error")