Monday, September 18, 2006
Posted on Monday, September 18, 2006 2:24:06 PM (Mountain Daylight Time, UTC-06:00)  Comments [4] | 
Categories: .NET | ArcGIS Devt | Team System
Things are going well with our ArcDAL project (Code Generation + Geodatabase = Goodness), and now that our base class library is pretty solid, I'm adding in unit tests. We want these tests to be run on every build, on any development system, so I want as much separation between the tests and any underlying required resources - for example - ArcSDE instances. Conveniently Scott Hanselman's podcast from last week was all about using Mock objects in unit testing.

What is a Mock?
Basically, a mock object is a "fake" object that is created solely for the purpose of running a test. It's generated on the fly using some mocking framework, and you basically tell it what to expect in terms of requests (properties / methods) and what to return. One place where this is really handy is when you want to avoid binding your tests to a specific instance of ArcSDE, or a specific file location for a personal geodatabase. It can also be used to avoid having to make calls out to other ArcObjects if you don't actually need to.

I'll go through a simple example to show what I mean...

Example:
Before I go any further, let me say that I'm using Visual Studio 2005, and the MSTest framework, but this will be pretty much the same in VS2003, and/or NUnit.

We'll look at the tests for a Workspace Utility class...


This class just has 3 very simple shared methods which return information about an IWorkspace. The thing is that in order to test this with real instances IWorkspace, we'd need to actually connect to ArcSDE and a personal geodatabase. And this will make the tests brittle, as it's unlikely that we'll keep an instance up for a long time, and file paths will be different on different development machines.

Thus, we use Mock objects to "fake" the workspace. I've chosen to use the NMock2 framework for two basic reasons: a) free and b) works.

I've gone ahead and had Visual Studio create a set of tests for this class for me. This is a pretty handy feature of VS2005 - it looks at your classes and spits out a basic test harness to use as a starting point.

    '''<summary>

    '''A test for IsAccessWorkspace(ByVal ESRI.ArcGIS.Geodatabase.IWorkspace)

    '''</summary>

    <TestMethod()> _

    Public Sub IsAccessWorkspaceTest()

        Dim workspace As IWorkspace = Nothing

        Dim expected As Boolean

        Dim actual As Boolean

        actual = ArcDAL.WorkspaceUtil.IsAccessWorkspace(workspace)

        Assert.AreEqual(expected, actual, "Did not return the expected value.")

    End Sub


Now, if we were doing this without mocks, we'd need to actually connect to a personal geodatabase, and pass that into our WorkspaceUtil class. Like this...

    '''<summary>

    '''A test for IsAccessWorkspace(ByVal ESRI.ArcGIS.Geodatabase.IWorkspace)

    '''</summary>

    <TestMethod()> _

    Public Sub IsAccessWorkspaceTest()

        Dim workspacefactory As IWorkspaceFactory = New AccessWorkspaceFactoryClass

        Dim connectionProperties As IPropertySet = New PropertySetClass

        connectionProperties.SetProperty("DATABASE", "C:\Test Data\ArcDAL\SampleData.mdb")

        Dim workspace As IWorkspace = workspacefactory.Open(connectionProperties, 0)

        Dim expected As Boolean

        Dim actual As Boolean

        actual = ArcDAL.WorkspaceUtil.IsAccessWorkspace(workspace)

        Assert.AreEqual(expected, actual, "Did not return the expected value.")

    End Sub


But, now we have a hardcoded path in the test. Now, we could get crafty, and read the file location from a configuration file, but that's not the point of this post!

So - to add in Mock objects, add a reference to NMock2 and include/import the NMock2 namespace in your test class. To make things easier, I setup the mock workspace in the ClassStartup method which runs before the tests are executed.

    Private Shared _mockWorkspace As IWorkspace

    'Startup

    <ClassInitialize()> _

    Public Shared Sub MyClassInitialize(ByVal testContext As TestContext)

        Dim mocks As New NMock2.Mockery

        _mockWorkspace = CType(mocks.NewMock(GetType(IWorkspace)), IWorkspace)

    End Sub


So - this has created a member variable of type "IWorkspace" which is in reality a mock. At this point we just have a mock, but we tell the mock what to "expect" and how to respond in the tests themselves.

    '''<summary>

    '''A test for IsAccessWorkspace(ByVal ESRI.ArcGIS.Geodatabase.IWorkspace)

    '''</summary>

    <TestMethod()> _

    Public Sub IsAccessWorkspaceTest_Mock()

        Expect.Once.On(_mockWorkspace).GetProperty("Type").Will(NMock2.Return.Value(esriWorkspaceType.esriLocalDatabaseWorkspace))             

        Dim workspace As IWorkspace = _mockWorkspace

        Dim expected As Boolean = True

        Dim actual As Boolean

        actual = ArcDAL.WorkspaceUtil.IsAccessWorkspace(workspace)

        Assert.AreEqual(expected, actual, "Did not return the expected value.")

    End Sub


Hopefully the word-wrap cleans up that line, but it's pretty simple, and follows a conversational type of logic.

Expect.Once.On(_mockWorkspace).GetProperty("Type").Will(NMock2.Return.Value(esriWorkspaceType.esriLocalDatabaseWorkspace))



Translates into:

Expect one call on _mockWorkspace to a Property called Type, Return a value of esriLocalDatabaseWorkspace

Pretty simple. The NMock framework takes care of actually "faking" out the IWorkspace, and then returns the specified value when the request it made. Here's a shot of the tests in the results viewer. All green!



So, while this is a very simple example of how to use Mock objects with ArcObjects, I'll be using this same methodology in other places as I add tests through the entire ArcDAL class library. I'm sure that there will be places where Mocks will not work (I suspect IFeature maybe a bit of a bear to mock), but it's a start.
Tuesday, September 19, 2006 8:11:41 AM (Mountain Daylight Time, UTC-06:00)
We have spent quite a bit of time discussing whether to mock ArcObjects in our unit tests. Sometimes it works great. Sometimes there is so much setup code you forget what you are testing.

In the instances where we skip the mocks and spin up a GeoDatabase, I have found the use of embedded resources in VS2005 a handy way to get around the hard-coded path problem. Add the .mdb (and even an .mxd) to Properties.Resources in the test project, copy it to a temporary location on [SetUp], and delete it on [TearDown]. You can even go as far as spinning up a MapControl with the .mxd, though test performance can get painful quickly.

Colby
Tuesday, September 19, 2006 8:29:45 AM (Mountain Daylight Time, UTC-06:00)
Colby -

I see the same thing - where mocking works, it's great, but I don't see any way to totally avoid spinning up something.

We do most of our work on SDE, so I've been looking at using Xml Schema dumps to allow us to spin up an entire ArcSDE database, but have not gotten to the point of implementing it. I'm going to look at either doing it in our test assemblies (but we'd likely want to use it for a whole set of test classes, so I need another hook besides Startup & Teardown), or creating build tasks which would create / destroy a GDB. I'll post on this when I get to it.

It's good to hear that others are actually doing unit testing with ArcObjects!

Cheers,

Dave
Tuesday, September 19, 2006 9:28:30 AM (Mountain Daylight Time, UTC-06:00)
Of course this is why the magicians at TypeMock http://www.typemock.com invented the TypeMock framework.
Scott also talks about it in his podcast.

I have just tries it and you can fake almost everything. From the hard coded path, to objects that are created deep within the code (and have no way to inject a fake one from the tests)
Richard Tell
Sunday, October 08, 2006 6:35:29 PM (Mountain Daylight Time, UTC-06:00)
I know what you mean, you have to be a "Mock Jock" to mock some of the complicated ArcObjects. I have found serializing/deserializing to XML to be a handy way to recreate the objects necessary for my unit tests. Since not all ArcObjects support the IXMLSerialize interface, I usually can fill in the gaps with nMock.

David
David McCourt
Comments are closed.