diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 896cc7fd0..2d1240bc0 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -492,6 +492,14 @@ func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args . return LessOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) } +// Matchesf asserts that the actual value meets the condition specified by matcher. +func Matchesf(t TestingT, matcher Matcher, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Matches(t, matcher, actual, append([]interface{}{msg}, args...)...) +} + // Negativef asserts that the specified element is negative // // assert.Negativef(t, -1, "error message %s", "formatted") diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index 8aaa25967..7a24e0f98 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -976,6 +976,22 @@ func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...i return Lessf(a.t, e1, e2, msg, args...) } +// Matches asserts that the actual value meets the condition specified by matcher. +func (a *Assertions) Matches(matcher Matcher, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Matches(a.t, matcher, actual, msgAndArgs...) +} + +// Matchesf asserts that the actual value meets the condition specified by matcher. +func (a *Assertions) Matchesf(matcher Matcher, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Matchesf(a.t, matcher, actual, msg, args...) +} + // Negative asserts that the specified element is negative // // a.Negative(-1) diff --git a/assert/assertions.go b/assert/assertions.go index bc15101b0..efa9d3daf 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -2066,3 +2066,28 @@ func buildErrorChainString(err error) string { } return chain } + +// A Matcher is an object that can assert and describe an arbitrary complex condition. +// This allows the assert framework to be extended in specialised ways while maintaining +// clear error reporting. +type Matcher interface { + // Match returns true if the given actual value meets the condition embodied + // in this Matcher. + Match(actual interface{}) bool + // Describe is called when the test fails to get a descriptive error message. + // It should succinctly describe the condition that is being expected. + Describe() string +} + +// Matches asserts that the actual value meets the condition specified by matcher. +func Matches(t TestingT, matcher Matcher, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if matcher.Match(actual) { + return true + } + _, act := formatUnequalValues(nil, actual) + return Fail(t, fmt.Sprintf("Not matching:\nexpected: %s\nactual : %s", matcher.Describe(), act), msgAndArgs...) +} diff --git a/assert/assertions_test.go b/assert/assertions_test.go index d2a25c245..83e80d435 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -3145,3 +3145,20 @@ func TestIsNil(t *testing.T) { t.Fatal("fail") } } + +func TestMatcher(t *testing.T) { + for _, tt := range []struct { + str string + expected bool + msg string + }{ + {"foobar", true, ""}, + {"wibble", false, "Not matching:\nexpected: a string starting with \"foo\"\nactual : string(\"wibble\")\n"}, + } { + t.Run(tt.str, func(t *testing.T) { + mockT := new(captureTestingT) + res := Matches(mockT, StringStarting("foo"), tt.str) + mockT.checkResultAndErrMsg(t, res, tt.expected, tt.msg) + }) + } +} diff --git a/assert/example_matcher_test.go b/assert/example_matcher_test.go new file mode 100644 index 000000000..d332372ca --- /dev/null +++ b/assert/example_matcher_test.go @@ -0,0 +1,48 @@ +package assert + +import ( + "fmt" + "regexp" + "strings" +) + +// stringStartsWith is a type that implements Matcher to test the actual +// string has the expected prefix. +type stringStartsWith struct { + expected string +} + +func (s *stringStartsWith) Match(actual interface{}) bool { + str, isStr := actual.(string) + return isStr && strings.HasPrefix(str, s.expected) +} + +func (s *stringStartsWith) Describe() string { + return fmt.Sprintf("a string starting with %q", s.expected) +} + +// StringStarting returns a Matcher that asserts that the actual value +// is a string that has the expected prefix. +// +// Wrapping the matcher type in a factory function is just a little syntactic sugar for +// its use in assert.Matches +func StringStarting(e string) Matcher { + return &stringStartsWith{expected: e} +} + +// Remove trailing whitespace from the message string, because govet +// removes it from the example comment +var stripTrailingSpace = regexp.MustCompile("(\\s+)\n") + +func ExampleMatcher() { + t := &captureTestingT{} // Usually this would be the *testing.T provided by the test + + Matches(t, StringStarting("goodbye"), "hello world") + + fmt.Println(stripTrailingSpace.ReplaceAllString(t.msg, "\n")) + // Output: + // Error Trace: + // Error: Not matching: + // expected: a string starting with "goodbye" + // actual : string("hello world") +} diff --git a/require/require.go b/require/require.go index fde08337c..f7b039e5e 100644 --- a/require/require.go +++ b/require/require.go @@ -1235,6 +1235,28 @@ func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...inter t.FailNow() } +// Matches asserts that the actual value meets the condition specified by matcher. +func Matches(t TestingT, matcher assert.Matcher, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Matches(t, matcher, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Matchesf asserts that the actual value meets the condition specified by matcher. +func Matchesf(t TestingT, matcher assert.Matcher, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Matchesf(t, matcher, actual, msg, args...) { + return + } + t.FailNow() +} + // Negative asserts that the specified element is negative // // assert.Negative(t, -1) diff --git a/require/require_forward.go b/require/require_forward.go index 8fddd1f7a..876527b0f 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -977,6 +977,22 @@ func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...i Lessf(a.t, e1, e2, msg, args...) } +// Matches asserts that the actual value meets the condition specified by matcher. +func (a *Assertions) Matches(matcher assert.Matcher, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Matches(a.t, matcher, actual, msgAndArgs...) +} + +// Matchesf asserts that the actual value meets the condition specified by matcher. +func (a *Assertions) Matchesf(matcher assert.Matcher, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Matchesf(a.t, matcher, actual, msg, args...) +} + // Negative asserts that the specified element is negative // // a.Negative(-1)