Vagif's blog post and ensuing discussion online did not go unnoticed in the office, and one of the first things we did this week was to discuss possible API designs for this feature. Basically, we are trying to do two things:
- Provide the user with an intuitive way to say "I want to match all values I passed into WhenCalled()" - this means the behavior set after WhenCalled() will happen only if all arguments passed to the actual call exactly match those passed to WhenCalled(). We feel this is what most users want and need.
- Provide the user with the ability to define customized arguments matching - this means the behavior set after WhenCalled() will only happen if the custom argument matching evaluated to be true.
We brainstormed several ideas and suggestions around the office, and came up with the following API:
| Exact argument matching: Return 1 only if the arguments passed to Foo() exactly match those passed to WhenCalled() (i.e are "hello", "world" and 9) | Isolate.WhenCalled(() => fake.Foo("hello", "world", 9)). |
| Custom argument matching: Return 1 only if the arguments passed to Foo() behave as follows: - the first string contains the world "hell" - the integer value is between 7 and 10 - ignore the second string | Isolate.WhenCalled((string s, int i) => fake.Foo(s, "", i)). |
| Custom and exact argument matching: Return 1 only if the first string contains "hell", other arguments match exactly to those passed to WhenCalled() | Isolate.WhenCalled((string s) => fake.Foo(s, "world", 9)). |
As you can see we have two new methods following WhenCalled(), and one change to WhenCalled() itself:
- WithExactArguments() - this method changes the behavior of WhenCalled() from the default of disregarding arguments to exactly matching arguments
- WhenCalled() will be changed to have overloads that receive Func<Res, T1,..,Tn> (and Action<T1,..,Tn>). This way the user can pass in typed parameters - this borrows heavily from Vagif's suggestion. In our example these are (string s, int i), which replace the arguments passed to the method in WhenCalled().
- AndArgumentsMatch() - this method receives a Func<bool, T1..Tn>, meaning it receives the same typed parameters WhenCalled() received. In this delegate the user can implement any argument check he needs on these parameters; in our case this is (s, i) => s.Contains("hell") && i >= 7 && i <= 10.
- The two extensions may be used together i.e WhenCalled().WithExactArguments().AndArgumentsMatch() - the use case here is exactly matching all arguments to the values passed in WhenCalled(), except those passed in as typed parameters which are handled in the custom matching in AndArgumentsMatch()
I think this API builds on the existing one without changing its default behavior and breaking older tests, and adds the capability to behave conditionally. What do you think? Your feedback and suggestions are highly appreciated.
Sounds good to me :-)
ReplyDeleteHow the following call will be interpreted:
Isolate.WhenCalled((string s, int i) => fake.Foo(s, "", i)).WillReturn(1);
Since there is no "WithExactArguments", will all three arguments be unmatched?
And this one:
Isolate.WhenCalled((string s, int i) => fake.Foo(s, "", i)).WithExactArguments().AndArgumentsMatch((s, i) => true).WillReturn(1);
Is this a way to match only the middle parameter (an empty string)?
Did you discuss assigning ref/out values? You probably read my 3rd post where I tried to figure out how this can be implemented, but with your current approach I guess it will have to be another chained call, e.g. ".WithOutput(...)".
Vagif
Isolate.WhenCalled((string s, int i) => fake.Foo(s, "", i)).WillReturn(1);
ReplyDeleteis the same as
Isolate.WhenCalled(() => fake.Foo("", "",1)).WillReturn(1);
Isolate.WhenCalled((string s, int i) => fake.Foo(s, "", i)).WithExactArguments().AndArgumentsMatch((s, i) => true).WillReturn(1);
is the same as
Isolate.WhenCalled((string s, int i) => fake.Foo(s, "", i)).WithExactArguments().WillReturn(1);
and the same as
Isolate.WhenCalled((string s) => fake.Foo("", s, 1)).AndArgumentsMatch(s=>s=="").WillReturn(1);
Note: The first release will contain only WithExactArguments(); without any arg matching
Yes, sounds logical.
ReplyDeleteI think xxx.WithExactArguments().AndArgumentsMatch(...) is too verbose. Why not just xxx.WithExactArguments([overload with lambda here]) ?
ReplyDeleteAs for the lambda itself, I'd expect it to take ALL method parameters. The second suggestion in the table sounds like a way to go.
This suggestion to put the checker lambda in WithExactArguments() came up, but it conflicts with the current default behavior of ignoring argument values, which we would like to preserve. Putting the checker lambda in WithExactArguments() implies that all argument values are checked, which is not what we're trying to do. It also raises problems such as what happens if you have 15 arguments and only want to check one of them - you would have to pass all 15 into the custom checker lambda, something we would like to avoid in sake of readability.
ReplyDeleteThe behavior we are planning is as follows:
- If you use WithExactArguments() alone it means ALL parameter values in WhenCalled() are checked
- If you use AndArgumentsMatch() with a checker lambda alone it means that if invoking the lambda returns true the behavior will apply, regardless of arguments in WhenCalled()
- If you use WithExactArguments().AndArgumentsMatch() together it means that you want to match all literal argument values specified in WhenCalled(), as well as run a checker lambda.
Doron, sounds like an exaggeration. If a method with 15 arguments is a pain to mock, it's just one more reason to refactor it.
ReplyDeleteToo bad that it's not possible to implement that with a paramless WithExactArguments all params get checked, while with a WithExactArguments(func[bool, ...]) they get checked as per bool result of the func.
I understand WithExactArguments(...).AndArgumentsMatch(...) is a good API for newbies - but, syntax-wise, it's clumsy as it repeats the same thing twice. What does it mean "with exact arguments and argumens match zzz"? why not just "with exact arguments zzz"?
And, re verbosity - people don't really like expectations and setups being too long.
Just to make sure I understand, is this your suggestion? How would you treat the 3rd case?
ReplyDelete- for exact matching of all arguments - .WithExactArguments()
- for exact matching of literal arguments and custom checking of parameterized arguments - .WithExactArguments(Func[bool, T1..Tn])
- for matching only based on checker lambda, where argument literals are ignored - ??
Don't allow ignoring argument literals.
ReplyDeleteI saw your point above "...you would have to pass all 15 into the custom checker lambda, something we would like to avoid in sake of readability."
And, as I said, "If a method with 15 arguments is a pain to mock, it's just one more reason to refactor it."
So 3rd case would be (notice s2 is not used),
Isolate
.WhenCalled(() => fake.Foo(null, null, null))
.WithExactArguments((string s, string s2, int i) => s.Contains("hell") && i <= 10 && i >= 7)
.WillReturn(1);
Andreister,
ReplyDeleteThe problem with refactoring a method with 15 arguments is that you don't always fake your own methods. Originally I also thought that passing the whole argument list wouldn't be a problem, but then I tried on a few real tests... We are all spoilt by Intellisense, and this case you don't get any Intellisense support.
In my 3rd post about TypeMock with lambda I had the following example:
Isolate.WhenCalled((string a, Evidence b, byte[] c, AssemblyHashAlgorithm d) => Assembly.LoadFrom(a, b, c, d)).WillReturn(null);
Isn't is a pain to write the argument list when you want exact match of all arguments?
Well, have to agree. It's a pain.
ReplyDeleteAnother case we took into consideration is the case of chained calls. You sometime want to ignore the parameters in the chain until the last call. How would the following if you include it in WithExactArguments()?
ReplyDeleteIsolate.WhenCalled((int a, int b) => fake.GetFactory("").GetProduct("").CalculateCost(a, b)).
AndArgumentsMatch( (a, b) => a < 100 && b % 2 == 0).
WillReturn(100)
I like the second method best...3rd is as many said 'too verbose', one thing to consider is something where you have 3 arguments and you want to ensure the 1st and 3rd arguments are exact matches and middle argument can be anything...I like the syntax of Arg.IsAny used in another framework to do this.
ReplyDeleteHow will this transpose to methods that have ref and out arguments (as this has plagued me lately in legacy code?)
I'd like to see the flip side to of 'Isolate.Verify.WasCalled' - I may not need to fake the input, but I may want to verify something was called with is level of detail. And again, on the Verify side, how would this transpose to ref and out arguments.