Writing Your Own Stubber, A How To Guide
The largest mystery when trying to understand how a mocking framework works has always been just understanding where this object it creates comes from. I mean I give the mocking framework an interface and it comes back with an object that implements everything (at least in the case of a stub). The point of this post is to show that they aren't magical and to hopefully extend what you think is possible in your own problem solving with C#. I'll be assuming you're familiar with mock objects and how/why they're used.
I developed a very small stub generating utility using TDD in order to explore the concepts that go into the big names such as Rhino.Mocks. This is the first test I wrote:
Hopefully that gives you an idea of how it is used. All of the tests you will see are centered around this single method call
MockedObject = Mockery.Mock<IFoo>();
No matter what interface I'm stubbing out the call to my library will be the exact same. Here's a more complicated set of examples:
Here are the rest of the use cases I tested: http://github.com/jcbozonier/CrutchMocks/tree/master/MiniMock/TestProject/
You already get the mocking frameworks I'm sure so let's cut to the meat and potatoes here:
The "magic" of the stubbing is handled through reflection and by emitting IL code. Here's an English translation of that code:
- First we need to build our new type that will be implementing the interface we were given by the person using our code. We define this type as being public and as being a class. The null at line 25 tells the TypeBuilder that this class has no parent and line 26 tells the TypeBuilder that we're implementing the interface provided to us by the user.
- Next we need to get a list of all of the methods that we need to implement on our new type. To do this I needed to reflect into the interface type provided and grab all of the methods it declares. I do this on lines 28 and 29.
- If there aren't any methods we're done! Create that type and create a new instance of it! (lines 49 and 50).
- If there are methods things get trickier.
- First, if the method takes in parameters we need to generate a list of the types of those parameters so that we can write the corresponding IL. (Line 35)
- Each method needs to be built up according to whether or not it has a return value.
- Then just create our instance.
Now the hardest part to figure out was how to create a method body for this new class. Let's look at the simplest example first:
IL is Sexy Black Magic
So what's happening here?
- First, I'm defining my method. I tell the TypeBuilder to define a method with the following properties:
- same name as the one on my interface
- that the method should be public and virtual,
- that the return type is void
- that it will take in the provided set of types as parameters.
- Then we're going to generate IL that does absolutely nothing with any of this. :)
- We just generate a single IL instruction that just returns.
One thing that might seem magical is knowing what IL OpCodes to emit to get the desired effect. For that magic I just wrote an method in C#, compiled it, and opened it up as IL inside Reflector. The code it gave was a little verbose though so I tweaked it a bit to minimize things.
Now let's take a look at the slightly more complicated example where I actually need to return a value:
The only differences here are at lines 16 and 17. At line 16 I emit an instruction to declare a local variable for the value that will be returned. At line 17 I push this value onto the stack and then immediately afterwards I return it. There is a property on the IL Generator to have the system automatically initialize your local variables to their default values. This is great for my stubber because that's all I want it to do! w00t!
Other than that knowing what the hell to call and how to get these different type and method builders just took a lot of googling.
<tdd_rant>
One of the benefits of using TDD in this process is that I could easily try out a chunk of code just to get things working (a spike) and then i could slowly strip portions of it away that I suspected added no value (especially handing for minimizing the IL instructions). There are currently 12 tests or observations going on here. Because I know that all of my cases I tried to build have all been driven by those observations I can try making changes and I quickly tell how many of my expectations will fail.
For example, on line 67 I defined a single line method to detect if a method has parameters:
If I change that to this:
I get a failing unit test. The bonus is that I know exactly the use case that will fail (because it did ;).
When_executing_a_mocked_method_that_has_a_return_value_of_null_and_parameters (1 test), 1 test failed
It_should_return_the_default_value, Failed:
This is a full list of test cases for my code:
<TestProject> (12 tests), Success
TestProject (12 tests), Success
When_executing_a_method_on_an_interface_with_several_methods_with_no_return_values (1 test), Success
It_should_execute, Success
When_executing_a_method_that_returns_an_int_on_a_mocked_object_with_mutliple_methods (1 test), Success
It_should_return_the_default_value, Success
When_executing_a_method_that_returns_an_obj_on_a_mocked_object_with_mutliple_methods (1 test), Success
It_should_return_the_default_value, Success
When_executing_a_mocked_method_that_has_a_return_value_of_null (1 test), Success
It_should_return_the_default_value, Success
When_executing_a_mocked_method_that_has_a_return_value_of_null_and_multiple_parameters (1 test), Success
It_should_return_the_default_value, Success
When_executing_a_mocked_method_that_has_a_return_value_of_null_and_parameters (1 test), Success
It_should_return_the_default_value, Success
When_executing_a_mocked_method_that_has_a_return_value_of_type_bool (1 test), Success
It_should_return_the_default_value, Success
When_executing_a_mocked_method_that_has_a_return_value_of_type_int (1 test), Success
It_should_return_the_default_value, Success
When_executing_a_mocked_method_that_has_no_return_or
_parameters (1 test), Success
It_should_be_callable, Success
When_executing_a_mocked_method_that_has_NO_return_value_and_multiple_parameters (1 test), Success
It_should_execute_with_no_error, Success
When_instantiating_an_empty_interface (1 test), Success
It_should_return_an_object_of_the_same_type, Success
When_instantiating_an_interface_with_a_single_parameterless_method (1 test), Success
It_should_return_an_object_of_the_same_type, Success