Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mock: Allow custom matcher functions to return error strings displayed on a failed match #781

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

georgelesica-wf
Copy link

NOTE: This is a copy of #639 since that fork is now inaccessible. Review has taken place there.

Custom Matchers are super helpful, but the failure messages are less so. This PR allows those using MatchedBy to return a custom error response for a failed match to control the output on failure. Additionally, this cleans up the output for the closest call arguments.

For example, the current version of testify gives the following output for the following test

Test

type testMock struct {
	mock.Mock
}

func (m testMock) MyMethod(a *http.Request) string {
	args := m.Called(a)
	return args.String(0)
}

func TestUsingCustomMatcher(t *testing.T) {
	m := new(testMock)
	m.Test(t)
	m.On("MyMethod", mock.MatchedBy(func(r *http.Request) bool {
		return r.Host == "www.example.com"
	})).Return("hello")

	m.MyMethod(&http.Request{
		Host: "www.not-example.com",
	})
}

Output

=== RUN   TestUsingCustomMatcher
--- FAIL: TestUsingCustomMatcher (0.00s)
	mock.go:237: 
		
		mock: Unexpected Method Call
		-----------------------------
		
		MyMethod(*http.Request)
				0: &http.Request{Method:"", URL:(*url.URL)(nil), Proto:"", ProtoMajor:0, ProtoMinor:0, Header:http.Header(nil), Body:io.ReadCloser(nil), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:"www.not-example.com", Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:"", RequestURI:"", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:context.Context(nil)}
		
		The closest call I have is: 
		
		MyMethod(mock.argumentMatcher)
				0: mock.argumentMatcher{fn:reflect.Value{typ:(*reflect.rtype)(0x13998a0), ptr:(unsafe.Pointer)(0x1443d78), flag:0x13}}
		
		
		Diff: 0: PASS:  (*http.Request=&{ <nil>  0 0 map[] <nil> <nil> 0 [] false www.not-example.com map[] map[] <nil> map[]   <nil> <nil> <nil> <nil>}) not matched by func() bool
FAIL

Process finished with exit code 1

This PR now allows for
Test

type testMock struct {
	mock.Mock
}

func (m testMock) MyMethod(a *http.Request) string {
	args := m.Called(a)
	return args.String(0)
}

func TestUsingCustomMatcher(t *testing.T) {
	m := new(testMock)
	m.Test(t)
	m.On("MyMethod", mock.MatchedBy(func(r *http.Request) error {
		if r.Host != "www.example.com" {
			return fmt.Errorf("request not against www.example.com")
		}
		return nil
	})).Return("hello")

	m.MyMethod(&http.Request{
		Host: "www.not-example.com",
	})
}

Output

=== RUN   TestUsingCustomMatcher
--- FAIL: TestUsingCustomMatcher (0.00s)
	mock.go:237: 
		
		mock: Unexpected Method Call
		-----------------------------
		
		MyMethod(*http.Request)
				0: &http.Request{Method:"", URL:(*url.URL)(nil), Proto:"", ProtoMajor:0, ProtoMinor:0, Header:http.Header(nil), Body:io.ReadCloser(nil), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:"www.not-example.com", Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:"", RequestURI:"", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:context.Context(nil)}
		
		The closest call I have is: 
		
		MyMethod(mock.argumentMatcher)
				0: MatchedBy(func(*http.Request) error)
		
		
		Diff: 0: FAIL:  (*http.Request=&{ <nil>  0 0 map[] <nil> <nil> 0 [] false www.not-example.com map[] map[] <nil> map[]   <nil> <nil> <nil> <nil>}) request not against www.example.com
FAIL

Process finished with exit code 1

Changes to the method signature for argumentMatcher.Matches may lead to breaking existing
external uses of that function. Introducing a  new method used internally allows presenting
the error information out for internal testify functionality while not breaking other
existing uses of Matches.

var matchError error
switch {
case result[0].Type().Kind() == reflect.Bool:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make the code simpler if we changed to if ... else if ... else if ... else here? Reason being that 'target-less' switch statements aren't obvious in their order of execution if there is more than 1 possible match.

}

func (f argumentMatcher) String() string {
return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).Name())
return fmt.Sprintf("func(%s) %s", f.fn.Type().In(0).String(), f.fn.Type().Out(0).String())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't the %s formatting directives have the same effect as calling .String() on the input arguments, by definition?

if fnType.NumOut() != 1 || fnType.Out(0).Kind() != reflect.Bool {
panic(fmt.Sprintf("assert: arguments: %s does not return a bool", fn))

if fnType.NumOut() != 1 || (fnType.Out(0).Kind() != reflect.Bool && !fnType.Out(0).Implements(errorType)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the code be simpler if we extract this into its own method? Something like returnsBoolOrError()? We can then also add a few good unit tests focusing just on it.

@glesica glesica self-assigned this Feb 7, 2020
@glesica
Copy link
Collaborator

glesica commented Feb 7, 2020

This isn't a breaking change (at least I don't think so), so I'm going to bump it to next+1 so we can expedite the next release.

@glesica glesica modified the milestones: Next, Next + 1 Feb 7, 2020
@dolmen dolmen added the pkg-mock Any issues related to Mock label Mar 6, 2024
@dolmen dolmen changed the title Allow custom matcher functions to return error strings displayed on a failed match mock: Allow custom matcher functions to return error strings displayed on a failed match Mar 6, 2024
@dolmen dolmen added the mock.ArgumentMatcher About matching arguments in mock label Mar 22, 2024
@ian-h-chamberlain
Copy link

Hi, is there any interest in reviving this PR since it's been a few years that it was delayed? I'd imagine there might be some changes needed to work with e.g. #1578 so would it be better to have it in place before those changes go in?

I tried to take a look at the conflicts and they don't seem too bad, and I could really benefit from having these changes so I could take a stab at fixing it up and addressing some comments if it seems worth merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement mock.ArgumentMatcher About matching arguments in mock pkg-mock Any issues related to Mock
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants