Jun 07 2012

CRM2011 Automated Testing – Unit Testing

Published by at 9:50 am under .NET,Microsoft CRM

Ideally the code being tested should not rely on external resources, where such dependencies exist they should be mocked to isolate the code being tested form the external system. In the case of plugins, this means that testing units of code should not require the code to have a connection to an actual instance of CRM.

CRM2011 has improved the way that plugins are developed against CRM, especially with the introduction of the CrmSvcUtil.exe tool for creating early bound classes for entities in CRM that can be used alongside the CRM SDK assemblies. Additionally Microsoft has provided interfaces for accessing CRM and data provided by CRM to the plugin:

  • IOrganizationService
  • IPluginExecutionContext
  • ITracingService

This means it is simple to mock the CRM services to test the plugin in isolation.

However, mocking a whole execution context would be a bit of a lengthy exercise, especially the IPluginExectionContext object is different depending on what messages and what pipeline stages you are writing the plugin for.

Thankfully there is a project on Codeplex that makes this job simple: CrmPluginTestingTools. There are two parts to this tool:

  • A plugin that you register in CRM as you will register your final plugin (including configuring pre- and post-images) that serialises the plugin execution context to xml files.
  • A library of classes for de-serialising the xml files into objects that match the IPluginExecutionContext interface.

Once you’ve registered their plugin, perform the steps that you plugin will handle; grab the output xml files; head into Visual Studio and start crafting your unit tests.

I tend to use the de-serialized xml files in my unit tests as a starting point and then use the object model to update the IPluginExecutionContext objects to structure the data for the specific test.

The execution context is only half of the story, I also use Moq to create a mock IOrganizationService that code being tested will be given instead of an actually connection to CRM. By doing this I am able to isolate the code from the CRM server plus I can determine what will be returned without having to setup records in CRM. I can even mock any type of exception if I want to test that aspect of my code.

As with all my automated tests, the unit tests are built on top of Gallio/MbUnit testing framework. This means that I don’t need to think about how to run the tests, I know they will all work with TestDriven.net in Visual Studio, the Gallio Test Runner – for a more graphical UI on my workstation, and TeamCity continuous integration server.

Overall Process

Of course, the plugins I write and single-class monsters, instead I break up the code in logical units (classes/methods) of responsibility and I test each unit on its own. If a unit of functionality doesn’t require access to CRM or the plugin execution context then I don’t worry about mocking those for that specific test.

Solution Structure

This solution contains several projects, these being:

  • Crm2011.Common

This contains the early-bound classes generate by CrmSvcUtil.exe which are used by several solutions.

  • Crm2011.Plugins.Address

The plugin that is being written/tested.

  • Crm2011.Plugins.DeploymentPackage

The project for deploying the plugin into CRM. The steps and images registered in this project should match how the CrmPluginTestingTools plugin was registered.

  • Plugin.Unit.Tests

This contains the unit tests created for testing the plugin, as well as the serialized execution contexts used by the unit tests. This project also contains a .gallio file that is the project used to run the tests in Gallio; and a .msbuild file that is the MsBuild project used to compile and run the tests in TeamCity.

  • SerializePluginContext & TestPlugin

The CrmPluginTestingTools projects for de-serializing the xml execution contexts in the unit tests.

Examples

[TestFixture]
public class CompanyAddressPreDelete
{

	MyServiceProvider serviceProvider;
	IPluginExecutionContext pluginContext;

	[SetUp]
	public void Setup()
	{
		var context = TestContext.CurrentContext.Test.Metadata["Context"][0];
		var service = new Mock<iorganizationservice>();
		var assemblyPath = typeof(IndividualAddressPreCreateTests).Assembly.Location;
		var contextFile = Path.Combine(Path.GetDirectoryName(assemblyPath), @"Address\Contexts\Company\Delete\"+context+".xml");
		serviceProvider = new MyServiceProvider(service.Object, contextFile);
		pluginContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
	}

	[Test, Metadata("Context", "PreOperation-BothTicked-Active")]
	public void both_active_should_clear_parent()
	{
		var plugin = new Crm2011.Plugins.Address.PreAddressDelete();
		plugin.Execute(serviceProvider);

		Assert.Exists(pluginContext.SharedVariables, sv => sv.Key == "AddressChanges", "Address Changes not registered in SharedVariables");
		var changes = new SharedVariables(pluginContext.SharedVariables).AddressChanges;
		Assert.Count(2, changes);
		Assert.Exists(changes, x => x.Address == AddressType.Mailing && x.Change == ChangeType.Remove);
		Assert.Exists(changes, x => x.Address == AddressType.Invoice && x.Change == ChangeType.Remove);

		Assert.Exists(pluginContext.SharedVariables, sv => sv.Key == "MakeChanges", "Make Changes not registered in SharedVariables");
		Assert.IsTrue((bool)pluginContext.SharedVariables["MakeChanges"]);
	}
}

The above code is a single test fixture (class that contains unit tests); a SetUp method that is run before each test; and a single unit test. I have added Metadata attribute to the test method so the setup method can determine which execution context xml files should be de-serialized for the test. The first two lines of the test setup and execute the plugin, the rest of the test ensures that the ShareVariables on the execution context have been correctly set of the given data.

The following example indicates how the organization service can be mocked to ensure that the plugin is making the correct call to CRM:

[TestFixture]
public class CompanyPostDeleteTests
{
	Mock<iorganizationservice> service;
	MyServiceProvider serviceProvider;
	IPluginExecutionContext pluginContext;

	[SetUp]
	public void Setup()
	{
		service = new Mock<iorganizationservice>();
		var assemblyPath = typeof(IndividualAddressPreCreateTests).Assembly.Location;
		var contextFile = Path.Combine(Path.GetDirectoryName(assemblyPath), @"Address\Contexts\Company\Delete\PostOperation.xml");
		serviceProvider = new MyServiceProvider(service.Object, contextFile);
		pluginContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
	}

	[Test]
	public void deleted_mailing_and_invoice_address_should_clear_account_twice()
	{
		var parentId = Guid.NewGuid();
		pluginContext.SharedVariables.Add("MakeChanges", true);
		pluginContext.SharedVariables.Add("AddressChanges", "Mailing:Remove|Invoice:Remove");
		pluginContext.PreEntityImages.Add("Image", CreateTestAddress(parentId));


		var plugin = new PostAddressDelete();
		plugin.Execute(serviceProvider);

		service.Verify(s => s.Update(It.Is<entity>(e =>
			 e.Id == parentId &&
			 e.LogicalName == "account" &&
			 e.Attributes.Contains("address1_line1") && (string)e["address1_line1"] == ""
		)), Times.Once());

		service.Verify(s => s.Update(It.Is<entity>(e =>
			e.Id == parentId &&
			e.LogicalName == "account" &&
			e.Attributes.Contains("address2_line1") && (string)e["address2_line1"] == ""
		)), Times.Once());
	}
}

By using unit testing in isolation of CRM, it is possible to create the vast majority of a plugin without having to touch the CRM server itself (with the exception of creating the test serialized execution contexts). Not only that, once you have created the unit tests, it is possible to run them again and again without having to manually click through CRM, so if you make changes to entities in CRM you can easily re-run the CrmSvcUtil.exe to update your early-bound classes and then run the unit tests to verify that they still pass.

Related Posts

One response so far