TypeMock Isolator and matching faked method’s arguments – part 2

I was glad to see that my thoughts about figuring out the best syntax to control the match of faked method’s arguments haven’t been unnoticed by TypeMockians. Eli Lopian raised a few questions, and addressing them seems to be crucial for materialization of the whole idea.

1. Backward compatibility with existing tests. If WhenCalled(() => some_method(a, b, c)) will be interpreted as a call with exact argument match, existing tests will fail. Parameterless delegates in such calls will have to be replaced with parameterized ones, e.g. WhenCalled((x, y, z) => some_method(x, y, z)).

I was quick enough to respond that I would be willing to update all tests just to get the syntax right, but I admit such response is childish. You can’t force your customers to use many hours on fixing their tests just because you discovered great syntax. Of course you need to provide a compatibility mode. There are different ways to achieve that. The simplest is to use a global flag:

Isolate.DefaultArgumentMatchBehavior = ArgumentBehavior.ExactMatch;

Exact match can be used as default for backward compatibility, at least during transition period. Or argument match mode can be implemented as an attribute that can later be deprecated. I don’t think this looks very elegant (global flags can’t be elegant), this is more like a temporary solution to let tests survive until the next major TypeMock release.

If this sounds too messy, then alternative approach might be to place functionality based on lambda syntax in a new namespace. Perhaps this is cleaner. I can compare it with approach taken by LINQ developers that divided LINQ classes between two namespaces and placed Expression and its derivations in System.Linq.Expressions. Later in this post I will show new extensions that solve custom checkers, and the more such extensions come, the more it is justified to define a dedicated namespace for lambda-based argument match control. What can work best, I don’t really know. Anyway, I believe resolving backward compatibility is more like configuration issue that should not compromise syntax selection.

What I find however important is to let a developer set up the tests so the arguments would be matched by default. Already now indexers are matched, this change broke many of our tests (I like defining custom indexers), but I think this was a right thing. When mock framework encounters ar[1], I believe it should respect the index value unless tests are specifically configured to ignore it. The same behavior I expect from faked methods: mock framework should not ignore that a method DoSomething(a, b, c) is expected to be faked with arguments (a, b, c). I do agree that this may not be turned on by default for backward compatibility purpose, but I would like to have a way to turn it on – at least to unifier indexers’ and methods’ behaviour.

2. Support for custom checkers. This is perhaps the most important requirement. If we find binary approach to argument matchings (match/nomatch) not sufficient because in some cases we expect partial argument match, we should also expect support for matching with custom checkers, for example when a string value begins with certain pattern, numerical value is within a certain range etc. I agree that without providing an easy way to define custom checkers the idea that I described in my previous post will stay a purely academical excersise. So I started playing with example code.

MyIsolate.WhenCalled((int x, int y) => MyClass.GetSum(x, y)).WillReturn(6);

So how we can extend this code to support custom argument checkers, for example to fake a method only if “x” is greater than 0? Something like this:

MyIsolate.WhenCalled((int x, int y) => MyClass.GetSum(x, y) where x > 0).WillReturn(6);

Of course this code will never compile, but I admit I imported System.Linq namespace and spent some time shuffling “where” keyword around. Not to resolve it using LINQ – just to get some inspiration. Making mock framework depedent on LINQ would be really mad (and would never work), not to say that both “where” clause is just a syntactic sugar around Where method that requires support for IEnumerable. And what IEnumerable can have with argument matching?

However turning inspecting implementation of “where” paid of, because the its core part is a predicate delegate Func. This is what we need to extend our WhenCalled with support for custom checkers.

Remember our new WhenCalled overloads listed in the previous post:

public static IPublicNonVoidMethodHandler WhenCalled(Func func)
public static IPublicNonVoidMethodHandler WhenCalled(Func func); 
public static IPublicNonVoidMethodHandler WhenCalled(Func func);
…

If for each parameterized Func delegate we provide an additional overload that takes a predicate delegate, then we enable custom checkers.

public static IPublicNonVoidMethodHandler WhenCalled(Func func);
public static IPublicNonVoidMethodHandler WhenCalled(Func func)
public static IPublicNonVoidMethodHandler WhenCalled(Func func, Func predicate)
public static IPublicNonVoidMethodHandler WhenCalled(Func func)
public static IPublicNonVoidMethodHandler WhenCalled(Func func, Func predicate)
…

Now look at that tests we can write now:

[Test, Isolated]
public void TestWithCustomCheckers()
{
    Isolate.WhenCalled((int x, int y) => MyClass.GetSum(x, y), (x, y) => x > 0).WillReturn(6);
    Assert.AreEqual(6, MyClass.GetSum(1, 2));
    Assert.AreEqual(1, MyClass.GetSum(-1, 2));

    Isolate.WhenCalled((int x, int y) => MyClass.GetSum(x, y), (x, y) => x > 10 && y > 10).WillReturn(6);
    Assert.AreEqual(6, MyClass.GetSum(20, 20));
    Assert.AreEqual(10, MyClass.GetSum(5, 5));

    Isolate.WhenCalled((int y) => MyClass.GetSum(1, y), y => y == 3).WillReturn(7);
    Assert.AreEqual(7, MyClass.GetSum(1, 3));
    Assert.AreEqual(3, MyClass.GetSum(1, 2));

    Isolate.WhenCalled((int x) => MyClass.Elements[x], x => x == 10).WillReturn(9);
    Assert.AreEqual(9, MyClass.Elements[10]);

    Isolate.WhenCalled((string x, string y) => 
        MyClass.Concatenate(x, y), (x, y) => x.StartsWith("abc") && y.EndsWith("xyz")).WillReturn("bingo!");
    Assert.AreEqual("bingo!", MyClass.Concatenate("abc", "xyz"));
    Assert.AreEqual("ab", MyClass.Concatenate("a", "b"));
}

What do we gain with this syntax?

1. I think it is more readable. In fact, I think it is much more readable. Compare the line 19 with something like Isolate.WhenCalled(MyClass.Concatenate(null, null)).AndArgumentsMatch(1, Arg.StartsWith(“abc”)).AndArgumentsMatch(2, Arg.EndsWith(“xyz”)).WillReturn(“bingo!”).

2. I believe when we extend functionality, we should prefer using built-in language constructions over new methods definition. The advanage of Func delegate is that it is based on generics and if we use it to define predicates then the predicates will naturally support methods and properties of respective types. You can simply write “x == 10” or “x.StartsWith(“abc”)”.

3. You can take advantage of boolean logic using built-in language operators. Writing “x > 0 && y > 0” is equally simple as writing “x > 0 || y > 0”, but how would you specify the latter checker using method-based notation? AndArgumentsMatch(1, Arg.GreaterThan(0)).OrArgumentsMatch(2, Arg.GreaterThan(0)?

There is one unanswered question from Eli: handing “ref” and “out” arguments. I need to think how this can be resolved.

Note: This post was originally published at my old blog host and may contain old comments.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s