Applications Services Blog
Get the latest thought leadership and information about the role of Applications Services in an increasingly interconnected world at the HP Blog Hub.

Unit testing custom Microsoft Dynamics CRM code – Part 3

In my last post I gave an introduction to unit testing Dynamics CRM C# interfaces code with mock objects using Visual Studio 2012 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:

1. We send CRM a create request that contains an account with the correct name value.

2. We send CRM a create request that contains a task 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:

 

/// <summary>
/// Creates a new account with a given name and then creates a follow-up task linked to the account
/// </summary>
/// <param name="accountName">account name</param>
/// <param name="service">organization service</param>
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<Entity>())).Returns(idToReturn). The It.IsAny<Entity>() 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<Entity>(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 with 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 Retrieve requests for an account with a specific id and any ColumnSet value. We could match the specific ColumnSet value specified in the method under test, but using an It.IsAny match makes sure a change to the columns retrieved doesn't cause the test to fail.


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:

 

/// <summary>
/// Tests the CreateCrmAccount2 method
/// </summary>
[TestMethod]
public void CreateCrmAccount2_Test()
{
	//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.

 

Complete class files for the method to test and the testing method are attached at the bottom of this post.

 

In my next 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 the CRM SDK like PicklistAttributeMetadata.

 

syndication-footer.png

Leave a Comment

We encourage you to share your comments on this post. Comments are moderated and will be reviewed
and posted as promptly as possible during regular business hours

To ensure your comment is published, be sure to follow the community guidelines.

Be sure to enter a unique name. You can't reuse a name that's already in use.
Be sure to enter a unique email address. You can't reuse an email address that's already in use.
Type the characters you see in the picture above.Type the words you hear.
Search
About the Author
Microsoft Dynamics CRM solution architect, C# developer, MBA, husband, father, Auburn man. Follow me on Twitter @lucas_is.


Follow Us
The opinions expressed above are the personal opinions of the authors, not of HP. By using this site, you accept the Terms of Use and Rules of Participation