Catching Arguments with Rhino Mocks
I’ve noticed that in quite a few of my tests, I was using a mock simply to test one argument on one of the methods called. Maybe I was making sure it passed a correctly formatted string, the right date/time, or whatever. There was a lot of setup and overhead (even using the new AAA syntax in Rhino Mocks) just to catch one argument. While pairing with Josh Flanagan, he said, “Wouldn’t it be cool if you could just catch the argument rather than having to go through all the mock setup stuff? I think Jeffrey Palermo did something like this.” Sure enough, he was right. We found this post which was almost exactly what we wanted. It was a little behind the times, however, since it didn’t account for some .NET 3.5 features (namely lambda expressions and expression trees) nor the new AAA syntax. We decided to take a few minutes and update it and this is what we came up with!
CapturingConstraint
First, we decided to use the same approach Jeffrey did by using the Constraint functionality in Rhino Mocks and came up with this:
1: public class CapturingConstraint : AbstractConstraint{
2: private readonly ArrayList argList = new ArrayList();
3:
4: public override bool Eval(object obj)
5: {
6: argList.Add(obj);
7: return true;
8: }
9:
10: public T First<T>(){
11: return ArgumentAt<T>(0);
12: }
13:
14: public T ArgumentAt<T>(int pos){
15: return (T) argList[pos];
16: }
17:
18: public override string Message{
19: get { return ""; }
20: }
21:
22: public T Second<T>(){
23: return ArgumentAt<T>(1);
24: }
25: }
</p>
This class captures everything Rhino Mocks throws at it. You can access the parameters, in order, by using the ArgumentAt method. It’ll also do the casing for you using a Generic type. We also added convenience methods for the first and second arguments since 99% of the time that’s all we cared about.
CaptureArgumentsFor Extension Method
The next thing we did is to add an extension method to System.Object to allow you to specify which method’s arguments you wish to capture. Unfortunately we had to add an extension method to System.Object which is usually not a good thing to do, but our mocks could be of any type, so we couldn’t get more specific than that. Also, this extension method is only available in our unit testing code, so this crime again Extension Methods is fairly localized.
1: public static CapturingConstraint CaptureArgumentsFor<MOCK>(this MOCK mock, Expression<Action<MOCK>> methodExpression)
2: {
3: var method = ReflectionHelper.GetMethod(methodExpression);
4:
5: var constraint = new CapturingConstraint();
6: var constraints = new List<AbstractConstraint>();
7:
8: foreach( var arg in method.GetParameters())
9: {
10: constraints.Add(constraint);
11: }
12:
13: mock.Expect(methodExpression.Compile()).Constraints(constraints.ToArray());
14:
15: return constraint;
16: }
</p>
Usage Example
Finally, our test ends up a bit more simple and looks something like this:
1: [Test]
2: public void should_correctly_assemble_the_notification_batch_with_the_context_and_template_group()
3: {
4: Services.PartialMockTheClassUnderTest();
5:
6: var argCatcher = ClassUnderTest.CaptureArgumentsFor(a => a.ExecuteBatch(null, null));
7:
8: ClassUnderTest.Execute(_context);
9:
10: var batch = argCatcher.Second<UserMessageBatch>();
11:
12: batch.Resolver.ShouldBeTheSameAs(_context);
13: batch.Group.ShouldBeTheSameAs(ClassUnderTest.TemplateGroup);
14: }
</p>