Return verbose errors why expected method call did not match#97
Return verbose errors why expected method call did not match#97balshetzer merged 4 commits intogolang:masterfrom
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! |
balshetzer
left a comment
There was a problem hiding this comment.
Thanks for this change. I Agree that it should make failures easier to understand.
| // 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.
Here we can add an error explaining that this call didn't match because it was exhausted.
gomock/controller.go
Outdated
| if err != nil { | ||
| 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.
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.
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.
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.
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.
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.
You are right. That comment and function were redundant.
| }) | ||
| } | ||
|
|
||
| func TestExpectedMethodCall_CustomStruct(t *testing.T) { |
There was a problem hiding this comment.
Could you please explain in a comment why we need to test this case?
There was a problem hiding this comment.
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
| func (c *Call) matches(args []interface{}) error { | ||
| 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.
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.
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! |
| "testing" | ||
|
|
||
| "github.com/golang/mock/gomock" | ||
| "strings" |
There was a problem hiding this comment.
Cosmetics: std library imports usually reside in the first "import group"
There was a problem hiding this comment.
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 callafter this PR:
This PR contains
FindMatchmethod. 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
mobject 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.