In Part I, I described what chains are, and the challenge they pose for Isolator. As you’ve already gathered, I will use every opportunity to use lame puns, so the word of the day is:
Interpreta-chain
How does Isolator interpret chains?
Well, first let’s see how it interprets method calls in general. Isolator uses the profiler API to intercept calls by the CLR. Whenever a method call is invoked, Isolator intercepts it first and does one of the following operations:
- Call the original method
- Record an expectation
- Return a mocked value
Since this is already compiled code, there’s no signaling of an end of line. The only information it receives is the next call.
Let’s examine this code:
1 |
<span class="kwrd">using</span> (RecordExpectations rec = RecorderManager.StartRecording()){<span class="kwrd"> int</span> x = a.b().c(); rec.Return(5);} |
Isolator receives the calls in this order (the assignment is implicit):
1 |
XReturn = a.b() <span class="rem">// returns a default "temporary" return value XReturn</span>int x = XReturn.c() <span class="rem">// returns a default "temporary" return value x</span>rec.Return(5) <span class="rem">// substitutes the former x with 5.</span> |
Now look at this chain:
1 |
<span class="kwrd">int</span> x = a.b().c(d.e(f.g().h(2,i.j())); |
Can you guess which 3 method calls are invoked first? If you guessed a.b(), f.g(), and i.j() in this order, you are correct. You are probably also suffering from the same headache I had when trying to dive into chain land. So method calls come in a somewhat confusing order, until you understand the rules.
Identifica-chain
A chain has a head and a tail, which identify it. Knowing when a chain starts is easy – with the first call. But where does it end?
Let’s look at this code:
1 |
<span class="kwrd">using</span> (RecordExpectations rec = RecorderManager.StartRecording()){<span class="kwrd"> int</span> x = a.b().c(d.e(f.g().h(2,i.j())); rec.Return(5);} |
The writer of this code, evil-minded as he is, concentrates on one thing – returning 5 instead of the call to a.b().c(…) method. No expectations are set or examined on the other calls that are on the way, and that starts with a.b(). Rule #1 Isolator uses the Return call to identify the end of the chain, and set the return value on that last expectation.
Now let’s look at the following simpler code containing an error:
1 |
<span class="kwrd">using</span> (RecordExpectations rec = RecorderManager.StartRecording()){ x = a.b();<span class="rem"> // Missing call to rec.Return(...)</span><span class="kwrd"> int</span> y = d.e(); rec.Return(5);} |
The writer of this code, expert as he is, forgot to put a Return call following the first call. Isolator continues to receive the calls, and needs to decide if the chain has been broken. Rule #2 to identifying a chain is when the return value of the former call, is the target of this call. In this case, the returned value of a.b() is an instance, while the next call d.e() is static. A static call has no instance and therefore we can identify a chain break.
And, in the former case, the chain break actually points to an error in the code. This helps us give the correct notification to the user.
Rule #2 is also a generalization of the following case:
1 |
<span class="kwrd">using</span> (RecordExpectations rec = RecorderManager.StartRecording()){ a.b();<span class="rem"> // No need to return a value, it just returns void </span><span class="kwrd"> int</span> y = d.e(); rec.Return(5);} |
Since a.b() is a void call, there’s no need for rec.Return call. Rule #3 says: if a method returns void, it’s a chain breaker.
Next time, I’m going to discuss extension methods, and how they fit in chains.