Friday, February 2, 2007

Mocking ArcObjects

I'm an avid test-driven developer but I'm also an ArcObjects programmer. Do you think this is an oxymoron? Read on!

Yes, ArcObjects are very expensive to create. Yes, they are slow and consume a lot of infrastructure. But none of this matters. ESRI did test-driven developers a humongous favor when they took an interface-oriented approach to ArcObjects. By doing this, they enabled us to easily unit test our code.

Suppose you are about to write a simple C# method like the following. How do we set up a test for it?
public static void EditFeature(IFeature feature, string fieldName, object value)
{
feature.set_Value(feature.Fields.FindField(fieldName), value);
}

Simple enough, right? Wrong! To even get a IFeature, we have to create am IFeatureWorkspace and to get one of those we'll have to create an IWorkspaceFactory. If your setup is anything like mine, that'll take tens of seconds and even minutes. Local geodatabases would ease that pain but either way our test will not be a unit test.

Do not despair! There's a way to test our method without relying on anything external to that one line of code. There's a wonderful testing tool called mock objects and with them we can test that our ArcObjects code does exactly what we want it to do.

You can go here if you want to learn more about mock objects. Today I'm just here to introduce you to a simple example of using mock objects to test ArcObjects code.

My favorite mocking framework for .NET is Rhino Mocks. When we want to create mock objects using Rhino Mocks, our first step is usually to create a MockRepository:
MockRepository mocks = new MockRepository();

The next thing that we need to is create our mock objects:
IFeature mockFeature = mocks.CreateMock();
IFields mockFields = mocks.CreateMock();

I'll explain why we need to create a mock IFields object in a second. Once you have your mock objects, you can start setting up your expectations. In Rhink Mocks, this works a bit like the record button on your...well, what has a record button these days? Take another look at the method that we're testing. We set the Value of an IFeature by giving it the index of the field that we want to change. This expectation is expressed like so:
Expect.Call(mockFields.FindField("field")).Return(1);

This line tells our MockRepository to expect our code under test to call the FindField() method on the mockFields object. It also demands that the value passed in to the method has to bed "field". Finally, we return the value 1 when that method is called.

Our next expectation is that the value returned by FindField() is used to set the value of the field. This expectation states that we expect the field at index 1 to be set to the value, "value":
mockFeature.set_Value(1, "value");

The last thing that we need to do is to tell our mockFeature to return our mockFields whenever its Fields property is called:
SetupResult.For(mockFeature.Fields).Return(mockFields);

That's it! Let's tell our Mock Repository that we're done setting up our expectations:
mocks.ReplayAll();

We really just have one line of test code:
EditFeature(mockFeature, "field", "value");

If we did all of this correctly, our test will pass and we have just written an ArcObject method test-first.

No comments: