The basic problem is: Ref parameter values passed to mocked methods have the default values specified in the mock setup and not the values passed by the calling code.
The background
Take the following interface as an example.interface ISomething
{
void DoStuff(string a);
}
Now supposed this is mocked and some things need to be done based on the value of parameter a.Normally this is easily done:
public void Initialize()
{
mockSomething = MockRepository.GenerateStub();
mockSomething.Stub(s => s.DoStuff(Arg.Is.Anything())
.Do((Action)(arg =>
{
// Perform general action with arg
if (arg == "abc")
{
// Special case action for arg
}
}));
}
Every time something calls mockSomething.DoStuff, the code passed to the Do method will be executed and the parameter arg will contain whatever value was passed to DoStuff. This is routine stuff for RhinoMocks and works as expected.The setup
Now suppose the parameter for DoStuff was a ref.interface ISomething
{
void DoStuff(ref string a);
}
This is where things get a bit dicey. The interface is still mocked and some things still need to be done based on the value of parameter a. So, some minor syntax changes in the arguments constraints to handle the ref stuff for the compiler are done:private delegate void DoStuffDelegate(ref string a);
public void Initialize()
{
mockSomething = MockRepository.GenerateStub();
mockSomething.Stub(s => s.DoStuff(ref Arg.Ref(Is.Anything(), string.Empty).Dummy)
.Do((DoStuffDelegate)((ref arg) =>
{
// Perform general action with arg
if (arg == "abc")
{
// Special case action for arg
}
}));
}
The problem
The above code compiles. And it runs. It just doesn't run correctly. The problem is that second parameter in the Arg<T>.Ref method. It indicates a return result for the the value. The problem is RhinoMocks sets the parameter value to the return result before calling Do's method. In other words, in this example, arg will always be string.Empty. The code in Do will never be called with the values sent to DoStuff by the original caller.Looking at the call stack, I could see the original method call with the correct parameter values. Then it went into the RhinoMock and proxy code and then Do's method was called, clearly with the unexpected value.
Looking for solutions
Digging around, I found the WhenCalled method. This appears to be a bit earlier in the mock/proxy processing so I changed the test.public void Initialize()
{
mockSomething = MockRepository.GenerateStub();
mockSomething.Stub(s => s.DoStuff(ref Arg.Ref(Is.Anything(), string.Empty).Dummy)
.WhenCalled(invocation =>
{
var arg = (string)invocation.Arguments[0];
// Perform general action with arg
if (arg == "abc")
{
// Special case action for arg
}
}));
}
Nope. This didn't work either. The value for Arguments[0] has already been set to the return value.While searching around, I found other people asking about the same issue. In their cases they found alternative solutions based on constraints of when their methods were called and what values the parameters could have. With known values, constants can be used via hard coding. For example, instead of using Is.Anything() as above, Is.Equal("abc") can be used and the second parameter can be "abc". Everything is fine.
I was semi-successful with the special case by using this technique. But then I needed to do the special action and use Is.NotEqual("abc"). I ran into the same problem as with Is.Anything(). I didn't know the original value for arg.
The solution
Widening my search, I stumbled upon an old article by Ayende talking about the Callback method. He considered it for weird edge case parameter checking and indicated it shouldn't generally be used. As far as I could make out from his write-up, it's purpose is as an alternative to the ArgHaving nothing to lose, I changed my code to give it a try:
private delegate bool DoStuffCallbackDelegate(ref string a);
public void Initialize()
{
mockSomething = MockRepository.GenerateStub();
mockSomething.Stub(s => s.DoStuff(ref Arg.Ref(Is.Anything(), string.Empty).Dummy)
.Callback((DoStuffCallbackDelegate)((ref arg) =>
{
// Perform general action with arg
if (arg == "abc")
{
// Special case action for arg
}
return true;
}));
}
Hurrah! This worked!Since Callback's intent is to determine if the constraint on the stub is valid, it gets the original parameter values, rather than the return result.
And, yes, I realize I'm misusing the intent of Callback. But when nothing else works, you go with what does.
The conclusion
Since this met my needs, I stopped here. If this was a function rather than a method, I suppose a Do or Return method would have to be chained to the setup code after the Callback method in order to return a valid value for the stubbed function. Also note, if this stub should be ignored, then false can be returned from Callback instead of true. This would allow other stubs for the same method to be handled differently.I'm not sure if this is a bug, intended behavior or an unconsidered edge case that has, at least to some, unexpected behavior. Both Out and Ref methods have the return result. It makes sense to be mandatory for Out, but I think it should be optional for Ref. I can see cases where you'd want to stub in a particular value all the time. The current syntax supports this well. But I can also see cases where it shouldn't be changed, at least by the Ref handling code. An overloaded version of Ref without the parameter would work well. In any case, I don't think it should be set before WhenCalled and Do are are invoked. At a minimum it should be after and better yet only if the original value hasn't been changed by WhenCalled or Do.
Well, that's my latest story regarding RhinoMocks. I hope it helps someone.
No comments:
Post a Comment