Tuesday, July 29, 2008

TDDing a CRM 4.0 Plugin - Part 7

Well, hopefully that goes over all the basics of TDDing a CRM 4.0 Plugin.

After the first post I stopped doing some refactoring. There are also a LOT of tests cases not hit here. I'm not going to code them all out, but I will at least show you the resulting refactored code:


using System;
using System.IO;
using System.Reflection;
using System.Xml.Serialization;
using Microsoft.Crm.Sdk;
using NMock2;
using NMock2.Monitoring;
using NUnit.Framework;

namespace TDD_with_CRM_Plugin
{
[TestFixture]
public class PluginTestCases
{
#region Setup/Teardown

[SetUp] // executes at the beginning of each test case. Executed only once per TextFixture
public void Setup()
{
mock = new Mockery();
webService = mock.NewMock();
context = mock.NewMock();
crmService = mock.NewMock();
correlationID = Guid.NewGuid();
}

#endregion

private Mockery mock;
private IWebServiceWrapper webService;
private IPluginExecutionContext context;
private Guid correlationID;
private string resultingUrl = "http://someurl";
private ICrmService crmService;

private void SetupExpectationThatCreateCrmServiceIsCalled()
{
Expect.Once.On(context).Method("CreateCrmService").With(true).Will(Return.Value(crmService));
}

private void SetupExpectationThatCRMWebServiceWillBeCalledWithUpdatedDynamicEntity(
DynamicEntity dynamicEntityWithURL)
{
Expect.Once.On(crmService).Method("Update").With(new DynamicEntityMatcher(dynamicEntityWithURL)).Will(
new PluginRecursionAction(context));
}

private DynamicEntity CreateAndSetupExpectationsThatDynamicEntityIsUpdatedWithURLFromWebService(
DynamicEntity account)
{
DynamicEntity dynamicEntityWithURL = CloneDynamicEntity(account);
dynamicEntityWithURL["new_sharepointurl"] = resultingUrl;
return dynamicEntityWithURL;
}

private void SetupExpectationThatCustomWebServiceIsCalledWithAccount(DynamicEntity account)
{
Expect.Once.On(webService).Method("UpdateAccount").With(account).Will(Return.Value(resultingUrl));
}

private void SetupExpectationsThatCorrelationIDWillBeRead()
{
Expect.Once.On(context).GetProperty("CorrelationId").Will(Return.Value(correlationID));
}

public DynamicEntity GetSampleAccount()
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof (DynamicEntity));
return
(DynamicEntity)
xmlSerializer.Deserialize(new StreamReader(@"Sample Files\account.sample.xml").BaseStream);
}

private DynamicEntity CloneDynamicEntity(DynamicEntity account)
{
if (account == null)
throw new ArgumentNullException("account", "The supplied DynamicEntity cannot be null.");
if (String.IsNullOrEmpty(account.Name))
throw new ArgumentOutOfRangeException("account",
"The name of the DynamicEntity can not be null or blank.");
XmlSerializer xmlSerializer = new XmlSerializer(typeof (DynamicEntity));
MemoryStream memoryStream = new MemoryStream();
xmlSerializer.Serialize(memoryStream, account);
memoryStream.Seek(0, SeekOrigin.Begin);
DynamicEntity clonedDynamicEntity = (DynamicEntity) xmlSerializer.Deserialize(memoryStream);

return clonedDynamicEntity;
}

private DemoPlugin GetDependencyInjectedPlugin()
{
return new DemoPlugin(webService);
}

private void SetupExpectationsThatPostEntityImagePropertyBagIsRead(PropertyBag postEntityImages)
{
Expect.Once.On(context).GetProperty("PostEntityImages").Will(Return.Value(postEntityImages));
}

private PropertyBag CreatePropertyBagWithAccount(DynamicEntity account)
{
PropertyBag postEntityImages = new PropertyBag();
postEntityImages["postImage"] = account;
return postEntityImages;
}

private DynamicEntity CreateSampleAccountDEWithPopulatedValues()
{
// the account entity that fired the update
DynamicEntity account = new DynamicEntity("account");
account["accountid"] = new Key(Guid.NewGuid());
account["accountnumber"] = "someAccountNumber";
account["name"] = "accountName";
return account;
}

[Test]
public void Execute_HappyPathExecutesAsExpected()
{
DemoPlugin plugin = GetDependencyInjectedPlugin();

SetupExpectationsThatCorrelationIDWillBeRead();

DynamicEntity account = CreateSampleAccountDEWithPopulatedValues();
PropertyBag postEntityImages = CreatePropertyBagWithAccount(account);

SetupExpectationsThatPostEntityImagePropertyBagIsRead(postEntityImages);
SetupExpectationThatCustomWebServiceIsCalledWithAccount(account);
SetupExpectationThatCreateCrmServiceIsCalled();

DynamicEntity dynamicEntityWithURL =
CreateAndSetupExpectationsThatDynamicEntityIsUpdatedWithURLFromWebService(account);

SetupExpectationsThatCorrelationIDWillBeRead();
SetupExpectationThatCRMWebServiceWillBeCalledWithUpdatedDynamicEntity(dynamicEntityWithURL);

plugin.Execute(context);
mock.VerifyAllExpectationsHaveBeenMet();
}

[Test]
public void New_DefaultDependencyIsCreatedCorrectly()
{
DemoPlugin plugin = new DemoPlugin();
Assert.IsInstanceOfType(typeof (MyWebService), plugin.webService);
}

[Test]
public void New_DependencyInjectionTakes()
{
DemoPlugin plugin = new DemoPlugin(webService);
Assert.AreEqual(webService, plugin.webService);
}
}

public class DynamicEntityMatcher : Matcher
{
private readonly DynamicEntity leftSide;

public DynamicEntityMatcher(DynamicEntity leftSide)
{
this.leftSide = leftSide;
}

public override bool Matches(object o)
{
if (!(o is DynamicEntity))
return false;

DynamicEntity rightSide = (DynamicEntity) o;
if (leftSide.Name != rightSide.Name)
return false;

foreach (Property property in leftSide.Properties)
{
if (!(rightSide.Properties.Contains(property.Name)))
return false;
if (!(PropertiesAreEqual(leftSide[property.Name], rightSide[property.Name])))
return false;
}
return true;
}

private bool PropertiesAreEqual(object leftSideProperty,
object rightSideProperty)
{
if (leftSideProperty == null && rightSideProperty == null)
return true;
if (leftSideProperty == null)
return false;

if (leftSideProperty.GetType() != rightSideProperty.GetType())
return false;
if (leftSideProperty.GetType() == typeof (string))
return (string) leftSideProperty == (string) rightSideProperty;

PropertyInfo[] properties =
leftSideProperty.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo property in properties)
{
object leftValue = property.GetValue(leftSideProperty, null);
object rightValue = property.GetValue(rightSideProperty, null);
if (leftValue == null && rightValue != null)
return false;
if (leftValue != null && !leftValue.Equals(rightValue))
return false;
}
return true;
}

public override void DescribeTo(TextWriter writer)
{
writer.Write("DynamicEntities are equal");
}
}

public class PluginRecursionAction : IAction
{
private readonly IPluginExecutionContext context;

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

#region IAction Members

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

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

#endregion
}
}


And the actual plugin class, refactored into it's own file:


using System;
using Microsoft.Crm.Sdk;

namespace TDD_with_CRM_Plugin
{
public class DemoPlugin : IPlugin
{
private static Guid correlationID;
internal IWebServiceWrapper webService;

public DemoPlugin() : this(new MyWebService())
{
}

public DemoPlugin(IWebServiceWrapper webService)
{
this.webService = webService;
}

#region IPlugin Members

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();
}

#endregion

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;
}
}
}


And the IWebServiceWrapper interface and implementation put into their own class. Notice here that we never tested this class. We don't care about it's functionality with these tests. We just care that it's called.


using Microsoft.Crm.Sdk;

namespace TDD_with_CRM_Plugin
{

public interface IWebServiceWrapper
{
string UpdateAccount(DynamicEntity account);
}

public class MyWebService : IWebServiceWrapper
{
#region IWebServiceWrapper Members

public string UpdateAccount(DynamicEntity account)
{
return "";
}

#endregion
}
}



You'll probably notice rather quickly that there is a lot more code in our test than our actual plugin. Don't be turned off by this. It just means you are thoroughly testing your code. If you make a change to your plugin you can quickly verify it all functions as like you expected without having to deploy and run through a full regression test suite through the CRM UI, which can be significanly more painful than this up-front coding.

I hope this was useful to someone. If you have any questions or comments, please let me know.

=-}

1 comment:

Anonymous said...

Hi Matthew,

I have just came across your very useful blog when i googled around using "nunit crm 4 plugin" keyword.

I saw across your 7 articles, and for sure i would like to read them carefully,since i am tired of manually testing the CRM 4 Plugin, especially the "DeliverIncoming" message of the email entity.

I am not sure how can we mock up "DeliverIncoming" message of email entity. I saw DeliverIncomingEmailRequest from CRM SDK, but i stuck with the weird error messages.

I will communicate later within this blog, and please keep writing about TDD and CRM 4 plugin, since i am a plugin developer too.

This is the link to the issue, if you are interested : http://www.microsoft.com/communities/newsgroups/en-us/default.aspx?dg=microsoft.public.crm.developer&tid=4abdc066-c01f-4467-b9a9-42c8be9acd82&cat=&lang=en&cr=US&sloc=&p=1

Best Regards,

Hadi Teo.