Tuesday, July 29, 2008

TDDing a CRM 4.0 Plugin - Part 5

At the end of Part 4 I mentioned a critical flaw with the current code of our plugin. It will naturally recurse and cause a stack overflow. This is because the plugin that triggers off of the updating of an account also updates that account.

There are a few different ways you can prevent plugin recursion from happening. There are numerous sources that you can google for about how to do this. I'll be using the CorrelationID in the plugin context to catch this.

However, we're more curious about how to test this to make sure it doesn't happen in our production code.

At this point we have to get into specifics of the NMock library. While I'll discuss and show you the specific implementation of the test, I'll go over enough of the concepts that you should be able to use nearly any mocking framework to do the same.

First, let's review the test:


Expect.Once.On(crmService).Method("Update").With(new DynamicEntityMatcher(dynamicEntityWithURL));


Here we're telling the NMock framework that we "expect" that the crmService will be called with a specific instance of the DynamicEntity object (which is checked by our special DynamicEntityMatcher class we created during the last post).

We're going to modify this to tell NMock that when this happens, we expect something else to occur as a result. If we look at the definition of the With method we see it returns an IMatchSyntax interface. One of the methods on that interface is the Will method. The Will method takes an IAction interface. We've used an instance of this interface before:


Expect.Once.On(context).Method("CreateCrmService").With(true).Will(Return.Value(crmService));


Here our IAction is the result of the Return.Value() method and we're saying our method call (CreateCrmService) will return a specific value (crmService). So we just need to create a new IAction:


public class PluginRecursionAction : IAction
{
private readonly IPluginExecutionContext context;

public PluginRecursionAction(IPluginExecutionContext context)
{
this.context = context;
}

public void Invoke(Invocation invocation)
{
new DemoPlugin().Execute(context);
}

public void DescribeTo(TextWriter writer)
{
writer.Write("Plugin will recurse");
}
}


And then update our previous Expect to use this action:


Expect.Once.On(crmService).Method("Update").With(new DynamicEntityMatcher(dynamicEntityWithURL)).Will(new PluginRecursionAction(context));


Now if we run our test case we'll see something we saw a while back:

NMock2.Internal.ExpectationException: unexpected invocation of pluginExecutionContext.PostEntityImages

Knowing our code, this means it's going through and trying to run a second time. Success!

Let's now make our code a little smarter. Since we're going to use the CorrelationID on the context to determine if we're in a recursive call we need to setup the expectation that it will be read from our mock object:


Guid correlationID = Guid.NewGuid();
Expect.Once.On(context).GetProperty("CorrelationId").Will(Return.Value(correlationID));



In fact, we expect that it will be read twice. Once for the initial call and a second time for the result of our custom IAction. So our final code:


Guid correlationID = Guid.NewGuid();
Expect.Once.On(context).GetProperty("CorrelationId").Will(Return.Value(correlationID));
Expect.Once.On(context).GetProperty("CorrelationId").Will(Return.Value(correlationID));



We can run our test again, although it doesn't exactly provide useful information. It's a good habit though... You should see an unexpected call to the PostEntityImages property and two unexecuted expectations for the CorrelationID.

Now it's time to write the code. The basic principle is that I'll have a static variable in my plugin called "correlationID". I'll set this if it's empty to the current context's CorrelationID. If it's not empty, then I'll check to see if it matches the current context's CorrelationID. If it does, then I'm calling myself, if not, then it's something else.

Here is the final code I end up with. Review it at your own pace. Now, this is from a previous plugin I worked with. I just noticed that there is a case where if two updates are occuring at the same time or nearly the same time, it could be problematic. I need to work through some tests for this and see what happens. I would not take this code to be production worthy at this point. Again, I'm teaching TDD with CRM plugins here, not proper CRM plugin development. =-}


public void Execute(IPluginExecutionContext context)
{
if (IsRecursiveCall(context))
return;
DynamicEntity account = (DynamicEntity) context.PostEntityImages["postImage"];
string resultingUrl = webService.UpdateAccount(account);
ICrmService crmService = context.CreateCrmService(true);
account["new_sharepointurl"] = resultingUrl;
crmService.Update(account);
ClearCorrelationID();
}

private void ClearCorrelationID()
{
correlationID = Guid.Empty;
}

private bool IsRecursiveCall(IPluginExecutionContext context)
{
if (correlationID == Guid.Empty)
correlationID = context.CorrelationId;
else if (correlationID == context.CorrelationId)
return true;
return false;
}



Now with this code in place and I re-run the test, it passes. I'm reasonably happy at this point (with the caveat already mentioned) that this plugin should not recurse.

Stay tuned for the (possibly last) post in the series regarding how to mock the retrieval of an object from the CRM WebService.

=-}

No comments: