A plug-in is custom business logic that you can integrate with Microsoft Dynamics CRM 4.0 to modify or augment the standard behavior of the platform. This custom business logic can be executed based on a message pipeline execution model called Event Execution Pipeline. A plug-in can be executed before or after a MS CRM platform event. For example, you can create a plug-in to validate the attributes of an account entity before the create and update operations.
To create plug-ins, you need to create a normal .NET class library and reference the MS CRM SDK libraries. Then add a class that implements the Microsoft.Crm.Sdk.IPlugin interface.
public interface IPlugin { void Execute(IPluginExecutionContext context); }
Plug-in Unit Testing
In order to write unit tests for your plug-in, you need to create at least a mock of the IPluginExecutionContext. Depending on your plug-in implementation, you will also need to mock ICrmService or IMetadataService if you are calling IPluginExecutionContext.CreateCrmService or IPluginExecutionContext.CreateMetadataService.
There is the MS CRM Plug-in Debugger, which consists of a small EXE container that implements a mock of the IPluginExecutionContext interface. You could use this container to unit test your plug-ins. However, IMHO, I do not see any advantage in using it versus a unit test and a mock framework. I posted a comment on the CRM Team Blog: Testing CRM Plug-in asking about that, but didn't get a response yet.
To unit test a CRM plug in, you can use your favorite unit test framework (NUnit, MbUnit, Visual Studio Tests) and your favorite mock framework (Rhino Mocks, NMock, Typemocks). In this article, I will be using NUnit and RhinoMocks.
The Plug-in Code
In the following example, adapted from the "Programming Microsoft Dynamics CRM 4.0" book, the plug-in validates the account number attribute before saving the account entity.
public class AccountNumberValidator : IPlugin { public void Execute(IPluginExecutionContext context) { var target = (DynamicEntity) context.InputParameters[ParameterName.Target]; if (target.Properties.Contains("accountnumber")) { var accountNumber = target["accountnumber"].ToString(); var regex = new Regex("[A-Z]{2}-[0-9]{6}"); if (!regex.IsMatch(accountNumber)) { throw new InvalidPluginExecutionException("Invalid account number."); } } } }
The code above checks to see if the account number attribute is in the right format. If not, it throws an InvalidPluginExecutionException. Since we will register this plug-in as a pre-event of creating and updating the account entity, this exception will be handled by the CRM platform, and the create/update operation is aborted.
Writing the Plug-in Unit Test
The following code is a simple test using NUnit to verify that an InvalidPluginExecutionException is thrown when the account entity has invalid account number:
[Test] [ExpectedException(typeof(InvalidPluginExecutionException))] public void ShouldHandleInvalidAccountNumber([Values("", "AB123456", "A123456", "ABC123456", "AB-12345", "AB123456", "AB-123", "AB-1234", "aa-012345", "aa-000000", "Za-999999", "wW-936187")] string number) { // Create necessary mocks for the plug-in. var mocks = new MockRepository(); var context = mocks.DynamicMock<IPluginExecutionContext>(); // Creates a property bag for the plugin execution context mock. var target = new DynamicEntity(); target.Properties["accountnumber"] = number; var inputParameters = new PropertyBag(); inputParameters.Properties[ParameterName.Target] = target; // Set expectations of mocks. Expect.Call(context.InputParameters).Return(inputParameters).Repeat.Any(); mocks.ReplayAll(); // Test the plug-in using the context mock. IPlugin plugin = new AccountNumberValidator(); plugin.Execute(context); // Verify all the mocks. mocks.VerifyAll(); }
Now, we will go through all the details of this unit test:
- The ExpectedException attribute defines the type of exception that this test expects to be raised. In our case, it is an InvalidPluginExecutionException.
- This is a parameterized test that uses the Values attribute to define a set of invalid account numbers. This test will run once for each value that we define. The Values attribute is specific to NUnit, but other frameworks have similar mechanisms: MbUnit uses RowTest for example.
- We create a mock of the IPluginExecutionContext interface by using the MockRepository.DynamicMock
method. We are using a DynamicMock because we are only interested in a small piece of the functionality (InputParameters property of the context object). If we want a complete control of the mock object behavior, then we would use a StrickMock. For more information about the types of mocks that you can create with Rhino Mocks, see here. - The InputParameters property of the plug-in context, is a property bag that will contain the account number attribute. So, we create this property bag, and add the account number defined by the Values attribute parameter.
- Now, we set the expectations of the mock object. This step is called the Record state. When the InputParameters property is called, we expect it to return the property bag we created on the previous step. Note that we are using Repeat.Any() that means this property can be called more than once. In our test, we just want to make sure that InputParameters is called, no matter how many times.
- The Record state is finish by calling ReplayAll(). This will move to the Replay state.
- Now, we are ready to instantiate our plug-in object and call its Execute method using the plug-in context mock object.
- Finally, we call VerifyAll() method, to verify that the mock expectations were satisfied. In our case, it will make sure that InputParameters property was called during the Replay state.
We also should write a test to assert that no InvalidPluginExecutionException is thrown when using valid account numbers. I will not include this test here, but you can see it on the solution source code files.
Mocking the ICrmService Interface
In our previous test, we only need to mock the plug-in context interface. However, in more complex plug-ins, you might need to mock other interfaces such as the ICrmService. The CreateCrmService method of the IPluginExecutionContext creates an ICrmService object. If you use the CreateCrmService method on your plug-in, you will need to create a mock of ICrmService.
Our validate account number plug-in has been changed to also detect duplicate account numbers. If an account number already exists, then the validation will fail by throwing an InvalidPluginExecutionException. To verify that the account number exists, we query CRM using the ICrmService.Fetch method with a FetchXML query. The following code demonstrate these changes:
public class AccountNumberValidator : IPlugin { /// <summary> /// /// </summary> /// <param name="context"></param> public void Execute(IPluginExecutionContext context) { var target = (DynamicEntity) context.InputParameters[ParameterName.Target]; if (target.Properties.Contains("accountnumber")) { var accountNumber = target["accountnumber"].ToString(); // Validates the account number format. var regex = new Regex("[A-Z]{2}-[0-9]{6}"); if (!regex.IsMatch(accountNumber)) { throw new InvalidPluginExecutionException("Invalid account number."); } // Validates the account number is unique. using (var service = context.CreateCrmService(true)) { var query = string.Format(@"<fetch mapping='logical'> <entity name='account'> <attribute name='accountnumber' /> <filter> <condition attribute='accountnumber' operator='eq' value='{0}' /> </filter> </entity> </fetch>", accountNumber); var results = service.Fetch(query); var xdocument = XDocument.Parse(results); var existingNumbers = from item in xdocument.Descendants("accountnumber") select item.Value; if (existingNumbers.Count() > 0) throw new InvalidPluginExecutionException("Account number already exist."); } } } }
Now, we will create a unit test to verify that our plug-in detects duplicate account numbers.
[Test] [ExpectedException(typeof(InvalidPluginExecutionException))] public void ShoulRejectDuplicateAccountNumber() { // Create necessary mocks for the plug-in. var mocks = new MockRepository(); var context = mocks.DynamicMock<IPluginExecutionContext>(); var service = mocks.DynamicMock<ICrmService>(); // Creates a property bag for the plugin execution context mock. var target = new DynamicEntity(); target.Properties["accountnumber"] = "AB-123456"; var inputParameters = new PropertyBag(); inputParameters.Properties[ParameterName.Target] = target; // Set expectations of mocks. Expect.Call(context.InputParameters).Return(inputParameters).Repeat.Any(); Expect.Call(context.CreateCrmService(true)).Return(service); Expect.Call(service.Fetch(null)).IgnoreArguments() .Return(@"<resultset> <result> <accountnumber>AB-123456</accountnumber> </result> </resultset>"); mocks.ReplayAll(); // Test the plug-in using the context mock. IPlugin plugin = new AccountNumberValidator(); plugin.Execute(context); // Verify all the mocks. mocks.VerifyAll(); }
In the test above, we are using Rhino Mocks to create a mock for the ICrmService. This object will be returned by the CreateCrmService method of the plug-in execution context. We are also recording that when ICrmService.Fetch method is called, it will return a XML file containing a duplicated account number. This will simulate the CRM behavior of detecting that an account number already exists, and we can assert that our plug-in will fail the validation by throwing an exception.
I hope this post helps you to unit test your CRM plug-ins. Although I demonstrated it using NUnit and Rhino Mocks, you can use any unit testing framework (NUnit, MbUnit, Visual Studio Tests, etc.) and any mock framework (Rhino Mocks, NMock, Typemocks, etc.).