-
Notifications
You must be signed in to change notification settings - Fork 609
Return verbose errors why expected method call did not match #97
Conversation
This commit makes erroneous calls show their origin. Unexpected calls show where they happened. Missing calls and improper expectations show where they were set up in the tests. This information is printed after each error message in square brackets. This commit was originally: ooesili@76f75d8 and a PR for it: golang#25. But there were minor conflicts with master from 2 aug 2017: golang@13f3609, which I fixed.
… as a clickable link
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed, please reply here (e.g.
|
I signed it! |
CLAs look good, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this change. I Agree that it should make failures easier to understand.
gomock/callset.go
Outdated
} | ||
|
||
// Search through the unordered set of calls expected on a method on a | ||
// receiver. | ||
callsErrors := "" | ||
for _, call := range calls { | ||
// A call should not normally still be here if exhausted, | ||
// but it can happen if, for instance, .Times(0) was used. | ||
// Pretend the call doesn't match. | ||
if call.exhausted() { | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we can add an error explaining that this call didn't match because it was exhausted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
gomock/controller.go
Outdated
origin := callerInfo(2) | ||
ctrl.t.Fatalf("no matching expected call: %T.%v(%v) [%s]", receiver, method, args, origin) | ||
ctrl.t.Fatalf("no matching expected call: %T.%v(%v) [%s]\n%s", receiver, method, args, origin, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a preamble to the explanations? Something like "because".
Also, I've never really liked this error message. I think it's confusing. A more direct message for the user would be something like: "%T.%v(%v) at %s is unexpected because %s" or "Unexpected call %T.%v(%v) at %s because %s". What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that it is confusing and I prefer your second suggestion. Continuation in the last comment.
gomock/controller_test.go
Outdated
actualErrMsg := e.log[len(e.log)-1] | ||
for _, expectedErrMsg := range expectedErrMsgs { | ||
if !strings.Contains(actualErrMsg, expectedErrMsg) { | ||
e.t.Errorf("Expected the actual error message:\n'%s'\nto contain expected error message:\n'%s'\n", actualErrMsg, expectedErrMsg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a little verbose. Maybe something shorter? e.g. "Error message: got %q, want .%q."
%q quotes the string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sure, changed to:
e.t.Errorf("Error message:\ngot: %q\nwant to contain: %q\n", actualErrMsg, expectedErrMsg)
So that an example failing test would result in output:
controller_test.go:78: Error message:
got: "Unexpected call to *gomock_test.Subject.FooMethod([argument extra_argument]) at /ide/work/src/github.com/golang/mock/gomock/controller_test.go:89 because \nExpected call at /ide/work/src/github.com/golang/mock/gomock/controller_test.go:238 has the wrong number of arguments. Got: 2, want: 1"
want to contain: "Invalid number of arguments of call"
gomock/controller_test.go
Outdated
return 0 | ||
} | ||
|
||
// Without this method the string representation of a TestStruct object would be e.g. {%!!(MISSING)s(int=123) no message} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure this comment is correct? This looks like what you get if you pass it into a Sprintf without any formatting directive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. That comment and function were redundant.
gomock/controller_test.go
Outdated
}) | ||
} | ||
|
||
func TestExpectedMethodCall_CustomStruct(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please explain in a comment why we need to test this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 2 comments. If you find this test not needed, I may as well remove it.
- comment to a similar, but easier test: https://github.com/golang/mock/pull/97/files#diff-763a69b80e2e0fb14b7d265a0f71d31eR190
- comment to this test: https://github.com/golang/mock/pull/97/files#diff-763a69b80e2e0fb14b7d265a0f71d31eR249
gomock/call.go
Outdated
if len(args) != len(c.args) { | ||
return false | ||
return fmt.Errorf("Invalid number of arguments of call: %s. Set: %s, while this call takes: %s", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the error messages returned are worded as error messages to their respective functions. This makes sense in context but I think it doesn't result in the best final error message to the user. Perhaps, instead, they could be worded for the final use. For example. The errors could say something like "Expected call <call details> <mismatch>". Then, the final message will read something like:
Unexpected call <call details> because:
Expected call <call details> has the wrong number of arguments (<got> vs. <want>)
Expected call <call details> doesn't match argument #<index> (<got> vs <want>)
Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like your suggestions very much. I implemented them almost as you said. Only in the case of:
Expected call <call details> doesn't match argument #<index> (<got> vs <want>)
I took the liberty to put got
and want
in separate lines, because it seems easier to read when the mocked functions take complex arguments. Example from tests of my project which uses Gomock, where an argument is of type: http.Request
:
controller.go:132: Unexpected call to *myhttp32.MockHTTPClientInterface.Do([0xc42000af00]) at /ide/work/src/34-mock-http-client-with-gomock/http_client.go:85 because:
Expected call at /ide/work/src/34-mock-http-client-with-gomock/http_client_test.go:74 doesn't match the argument at index 0.
Got: &{GET some-url HTTP/1.1 1 1 map[] <nil> <nil> 0 [] false map[] map[] <nil> map[] <nil> <nil> <nil> <nil>}
Want: is equal to &{GET some-urla HTTP/1.1 1 1 map[] <nil> <nil> 0 [] false map[] map[] <nil> map[] <nil> <nil> <nil> <nil>}
controller.go:177: missing call(s) to *myhttp32.MockHTTPClientInterface.Do(is equal to &{GET some-urla HTTP/1.1 1 1 map[] <nil> <nil> 0 [] false map[] map[] <nil> map[] <nil> <nil> <nil> <nil>}) /ide/work/src/34-mock-http-client-with-gomock/http_client_test.go:74
controller.go:184: aborting test due to missing call(s)
I also changed the error message:
No expected method calls for that receiver
to
there are no expected method calls for that receiver
so that it reads better with the Unexpected call <call details> because:
@balshetzer, thank you for all the remarks. They are very helpful. I have now one problem: this line when printed in Gogland, does not result in output containing a clickable link to a particular path and line (the |
So the path and line are not a clickable link if they are in the same line as the path and line to E.g. here
whereas here it is not:
I don't want to dig into this, it is not that crucial. |
@xmik Thanks! |
@@ -20,6 +20,7 @@ import ( | |||
"testing" | |||
|
|||
"github.com/golang/mock/gomock" | |||
"strings" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cosmetics: std library imports usually reside in the first "import group"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, @pasztorpisti, I will stick to this advice in the future.
Hi, I believe this pull request accidentally made the error reporting incur quadratic costs due to the string concatenation in a tight loop routine. I noticed this when running HEAD of GoMock against a large test. According to PProf, the problem was this concatenation: https://github.com/golang/mock/blame/4187d4d04aa043124750c9250259ceafdc5f7380/gomock/callset.go#L68-L78 I have a fix out for review here: #103 |
Hi. Thanks for gomock! I just started using it and thought it was not enough verbose when it comes to errors messages.
The benefits
Before this PR, in my tests, I see an error:
no matching expected call
after this PR:
This PR contains
FindMatch
method. It now also returns an error which explains why there was no call match. I added tests for each error explanation message kind.You may not like
m
object is of typeMatcher
, and it has no public fields, so we can only use itsString()
method. I don't think it is greatly important.Please voice any concerns and treat me as unexperienced in Go, as I've just recently started using it.