How to unit test C# Dynamics CRM interface code - part II
Earlier this week I wrote a post that gave an introduction to unit testing Dynamics CRM C# interfaces code with mock objects using NUnit and Moq. The sample code in that post was extremely simple, so I wanted to follow up with a more complex example that shows how to test multiple calls to the CRM web service instead of just a single one.
This example supposes you have been asked to write a method that will take a company name as an input and then create a new CRM account. This method will also create a follow-up task linked to the account, and the subject of the task must contain an account number that is generated by a plug-in upon creation of the account.
First, let's think about how we would test this method before we start writing any code. After we have outlined our basic testing approach, writing the actual code will be much easier.
To test this method, we need to verify a few different things:
- We send CRM a create request that contains an account with the correct name value.
- We send CRM a create request that contains a tastk with the correct subject value (including the account number) and regardingobjectid value.
These two tests address the requirements, but they don't account for the reality that we need to make a retrieve request to CRM to get the account number upon account creation, so we should add a third test step to validate sending CRM a retrieve request for the account number using the account id value that is returned by creating the account.
Now that we have established what we have to test, we can write a method that makes the calls. It would look something like this:
public static void CreateCrmAccount2(string accountName, IOrganizationService service)
{
//create the account
Entity account = new Entity("account");
account["name"] = accountName;
Guid newId = service.Create(account);
//get the account number
account = service.Retrieve("account", newId, new Microsoft.Xrm.Sdk.Query.ColumnSet(new string[]{"name","accountid","accountnumber"}));
string accountNumber = account["accountnumber"].ToString();
//create the task
Entity task = new Entity("task");
task["subject"] = "Finish account set up for " + accountName + " - " + accountNumber;
task["regardingobjectid"] = new Microsoft.Xrm.Sdk.EntityReference("account", newId);
service.Create(task);
}
Unlike in my first mocking example, we have two different calls to the service Create method, so we have to tell our mock service how to handle each. Previously we used a Setup like this to handle all calls to the Create method - serviceMock.Setup(t => t.Create(It.IsAny())).Returns(idToReturn). The It.IsAny() tells the mock service to return idToReturn in response to a Create call made with any Entity object as an input parameter.
This time around we will use a Setup for each Create call that matches on Entity.LogicalName. Because the Setup takes a Linq expression as a parameter, this is how we match on the LogicalName value - Create(It.Is(e => e.LogicalName.ToUpper() == "account".ToUpper())).
We only have one Retrieve call to handle, but let's go ahead and be specific with it like we are being the the Create calls. The match for our task creation step would look like this:
Retrieve(
It.Is<string>(e => e.ToUpper() == "account".ToUpper()),
It.Is<Guid>(e => e == idToReturn),
It.IsAny<Microsoft.Xrm.Sdk.Query.ColumnSet>())
)
This matches all requests to Retrieve and an account with a specific id. I (out of laziness more than anything else) have decided not to match a specific ColumnSet, so that's why the columns parameter still uses It.IsAny.
With the Setup work complete, now it's just a matter of sending some mock data through the CreateCrmAccount2 method and making sure we handle the responses correctly. Here is the complete test method:
[Test]
public void CreateCrmAccount2()
{
//ARRANGE - set up everything our test needs
//first - set up a mock service to act like the CRM organization service
var serviceMock = new Mock<IOrganizationService>();
IOrganizationService service = serviceMock.Object;
//next - set a name and account number for our fake account record to create
string accountName = "Lucas Demo Company";
string accountNumber = "LPA1234";
//next - create a guid that we want our mock service Create method to return when called
Guid idToReturn = Guid.NewGuid();
//next - create an object that will allow us to capture the account object that is passed to the Create method
Entity createdAccount = new Entity();
//next - create an entity object that will allow us to capture the task object that is passed to the Create method
Entity createdTask = new Entity();
//next - create an mock account record to pass back to the Retrieve method
Entity mockReturnedAccount = new Entity("account");
mockReturnedAccount["name"] = accountName;
mockReturnedAccount["accountnumber"] = accountNumber;
mockReturnedAccount["accountid"] = idToReturn;
mockReturnedAccount.Id = idToReturn;
//finally - tell our mock service what to do when the CRM service methods are called
//handle the account creation
serviceMock.Setup(t =>
t.Create(It.Is<Entity>(e => e.LogicalName.ToUpper() == "account".ToUpper()))) //only match an entity with a logical name of "account"
.Returns(idToReturn) //return the idToReturn guid
.Callback<Entity>(s => createdAccount = s); //store the Create method invocation parameter for inspection later
//handle the task creation
serviceMock.Setup(t =>
t.Create(It.Is<Entity>(e => e.LogicalName.ToUpper() == "task".ToUpper()))) //only match an entity with a logical name of "task"
.Returns(Guid.NewGuid()) //can return any guid here
.Callback<Entity>(s => createdTask = s); //store the Create method invocation parameter for inspection later
//handle the retrieve account operation
serviceMock.Setup(t =>
t.Retrieve(
It.Is<string>(e => e.ToUpper() == "account".ToUpper()),
It.Is<Guid>(e => e == idToReturn),
It.IsAny<Microsoft.Xrm.Sdk.Query.ColumnSet>())
) //here we match on logical name of account and the correct id
.Returns(mockReturnedAccount);
//ACT - do the thing(s) we want to test
//call the CreateCrmAccount2 method like usual, but supply the mock service as an invocation parameter
MockDemo.CreateCrmAccount2(accountName, service);
//ASSERT - verify the results are correct
//verify the entity created inside the CreateCrmAccount method has the name we supplied
Assert.AreEqual(accountName, createdAccount["name"]);
//verify task regardingobjectid is the same as the id we returned upon account creation
Assert.AreEqual(idToReturn, ((Microsoft.Xrm.Sdk.EntityReference)createdTask["regardingobjectid"]).Id);
Assert.AreEqual("Finish account set up for " + accountName + " - " + accountNumber, (string)createdTask["subject"]);
}
Something to note when testing this is that it doesn't matter if the mock account number you use matches the format of your real account numbers. Because you are only pretending to generate / retrieve / pass the account number, you can make it anything you want without having to worry about whether this compromises your test.
A complete code sample is attached MockDemo.cs (7.78 kb). This includes the code sample for my introduction as well, so if you download and build this, you will have two tests to run instead of just one.
In my third (and probably last) post in this series, we'll take a look at using wrapper classes to help us test operations where we need to mock behavior that uses sealed classes in CRM SDK like PicklistAttributeMetadata. Until then, happy coding!