Posted on 2. December 2010

Unit Testing Dynamics CRM 2011 Pipeline Plugins using Rhino Mocks

It’s been a while since Dynamics CRM 2011 Beta 1 was released (surely we are due Beta 2 soon!) so I thought it was about time I set up a Unit Test framework for PlugIns. I’ve been using Rhino Mocks for a while to great effect, so here we go!

This example aims to unit test the SDK sample Plugins, and demonstrates the following:

    • Mocking the pipeline context, target and output property bags.
    • Mocking the Organisation Service.
    • How to assert that exceptions are raised by a plugin
    • How to assert that the correct Organisation Service method was called with the desired values.

To build the examples, you’ll need the CRM2011 SDK example plugins and Rhino Mocks 3.6 (http://ayende.com/Blog/archive/2009/09/01/rhino-mocks-3.6.aspx). 

The key principle of mocking is that we can exercise and examine the code that we need to test without executing the bits that are not being tested. By mocking we are fixing behaviour and return values of the dependant code so that we can assert if the results are what we expect.  This approach supports Test Driven Development (TDD), where the test is written first and then the desired functionality is added in order that the test passes. We can then say we are ‘Done’ when all tests pass. 

So in our example, the Followup Plugin should create a task with the regarding id set to the id of the account. So by mocking the pipeline context, we can specify the account id, and check that the resulting task that is created is regarding the same account. Using Rhino Mocks allows us to create a mock Organisation Service and assert that the Create method was called passing a task with the desired attributes set.

 

[TestMethod]
public void FollowupPlugin_CheckFollowupCreated()
{
    RhinoMocks.Logger = new TextWriterExpectationLogger(Console.Out);

    // Setup Pipeline Execution Context Stub
    var serviceProvider = MockRepository.GenerateStub();
    var pipelineContext = MockRepository.GenerateStub();
    Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext)
        serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext));
    serviceProvider.Stub(x => x.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext))).Return(pipelineContext);

    // Add the target entity
    ParameterCollection inputParameters = new ParameterCollection();
    inputParameters.Add("Target", new Entity("account"));
    pipelineContext.Stub(x => x.InputParameters).Return(inputParameters);

    // Add the output parameters
    ParameterCollection outputParameters = new ParameterCollection();
    Guid entityId= Guid.NewGuid();

    outputParameters.Add("id", entityId);
    pipelineContext.Stub(x => x.OutputParameters).Return(outputParameters);

    // Create mock OrganisationService
    var organizationServiceFactory = MockRepository.GenerateStub();
    serviceProvider.Stub(x => x.GetService(typeof(Microsoft.Xrm.Sdk.IOrganizationServiceFactory))).Return(organizationServiceFactory);
    var organizationService = MockRepository.GenerateMock();
    organizationServiceFactory.Stub(x => x.CreateOrganizationService(Guid.Empty)).Return(organizationService);

            
    // Execute Plugin
    FollowupPlugin plugin = new FollowupPlugin();
    plugin.Execute(serviceProvider);

    // Assert the task was created
    organizationService.AssertWasCalled(x => x.Create(Arg.Is.NotNull));

    organizationService.AssertWasCalled(x => x.Create(Arg.Matches(s => 
            ((EntityReference)s.Attributes["regardingobjectid"]).Id.ToString() == entityId.ToString()
            &&
            s.Attributes["subject"].ToString() == "Send e-mail to the new customer."
            )));
          
}

The key thing to notice is that the only mock object here is the OrganisationService - all others are stubs. The difference between a stub and a mock is that the mock records the calls that are made so that they can be verified after the test has been run. In this case we are verifying that the Create method was called with the correct properties set on the task entity.

It’s worth noting the RhinoMocks.Logger assignment. This gives the Rhino logging output in the VS2010 test results; most helpful during debugging asserts that don’t do as you expect.

Looking at the sample Account Plugin, it throws an exception when the account number is already set - so what testing that plugin’s throw exceptions under some conditions. Unfortunately, the standard VS2011 ExpectedExceptionAttribute doesn’t provide the functionality we need here since we can’t check exception attributes, nor can we run the tests in Debug without the Debugger breaking into the code even though the exception is marked as being expected. In order to get around this this I use a class written by Keith E. Burnell:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Kb.Research.RhinoMocks.UnitTests.CusomAssertions
{
    /// 
    /// Custom assertion class for unit testing expected exceptions.  
    /// A replacement for the ExpectedException attribute in MSTest
    /// 
    public static class AssertException
    {
        #region Methods

        /// 
        /// Validates that the supplied delegate throws an exception of the supplied type
        /// 
        /// Type of exception that is expected to be thrown
        /// Delegate that is expected to throw an exception of type 
        public static void Throws<TExpectedExceptionType>(Action actionThatThrows) where TExpectedExceptionType : Exception
        {
            try
            {
                actionThatThrows();
            }
            catch (Exception ex)
            {
                Assert.IsInstanceOfType(ex, typeof(TExpectedExceptionType), String.Format("Expected exception of type {0} but exception of type {1} was thrown.", typeof(TExpectedExceptionType), ex.GetType()));
                return;
            }
            Assert.Fail(String.Format("Expected exception of type {0} but no exception was thrown.", typeof(TExpectedExceptionType)));
        }

        /// 
        /// Validates that the supplied delegate throws an exception of the supplied type
        /// 
        /// Type of exception that is expected to be thrown
        /// Expected message that will be included in the thrown exception
        /// Delegate that is expected to throw an exception of type 
        public static void Throws<TExpectedExceptionType>(string expectedMessage, Action actionThatThrows) where TExpectedExceptionType : Exception
        {
            try
            {
                actionThatThrows();
            }
            catch (Exception ex)
            {
                Assert.IsInstanceOfType(ex, typeof(TExpectedExceptionType), String.Format("Expected exception of type {0} but exception of type {1} was thrown.", typeof(TExpectedExceptionType), ex.GetType()));
                Assert.AreEqual(expectedMessage, ex.Message, String.Format("Expected exception with message '{0}' but exception with message '{1}' was thrown.", ex.Message, expectedMessage));
                return;
            }
            Assert.Fail(String.Format("Expected exception of type {0} but no exception was thrown.", typeof(TExpectedExceptionType)));
        }

        #endregion

    }
}

So, we want to test that if the account number is already set on execution of the AccountNumberPlugin, then an exception is raised:

[TestMethod]
public void AccountNumberPlugin_CheckExceptionIfAccountNumberSetAllready()
{
    // Setup Pipeline Execution Context Stub
    var serviceProvider = MockRepository.GenerateStub();
    var pipelineContext = MockRepository.GenerateStub();
    Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext)
        serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext));
    serviceProvider.Stub(x => x.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext))).Return(pipelineContext);

    // Add the target entity
    ParameterCollection inputParameters = new ParameterCollection();
    inputParameters.Add("Target",new Entity("account") { Attributes = new AttributeCollection  { 
            new KeyValuePair("accountnumber", "123")
        }});

    pipelineContext.Stub(x => x.InputParameters).Return(inputParameters);

    // Test that an exception is thrown if the account number already exists
    AccountNumberPlugin plugin = new AccountNumberPlugin();
    AssertException.Throws < InvalidPluginExecutionException> ("The account number can only be set by the system.",(Action) delegate {
            plugin.Execute(serviceProvider);
            });

}

The examples above show how to test plugins that don’t call any external services or code - where all dependencies are discovered via the execution context. Next time I’ll provide an example of how to mock an external service using inversion of control.

 

 

Posted on 11. November 2010

Have you tried turning it off and on again?

Today I was trying to register a Plugin to Dynamics CRM 2011, and I received the following error:

System.Runtime.InteropServices.COMException: Microsoft Dynamics CRM has experienced an error.
 Reference number for administrators or support: #63F08CDB

Using the platform trace, I tracked it down to the following exception:

System.Runtime.InteropServices.COMException (0x800703FA):
Retrieving the COM class factory for component
with CLSID {E5CB7A31-7512-11D2-89CE-0080C792E5D8}
failed due to the following error: 
800703fa Illegal operation attempted on a 
registry key that has been marked for deletion.

From further digging, this turned out to be an issue to do with the installer having deleted a COM registration, but the registry hive not yet unloaded due to user locking it. To fix, I simply re-booted. Just goes to show you - as that great philosopher Roy Trennaman once said
'Hello, IT, have you tried turning it off and on again?'

Posted on 11. November 2010

Server Error in Application "MICROSOFT DYNAMICS CRM" HTTP Error 404.0 - Not Found

After installing a CRM 4.0 ISV Add-on to my Dynamics CRM 2011 Beta 1 server (foolish I hear you say!), I was presented with the following:

 

Server Error in Application "MICROSOFT DYNAMICS CRM"Internet Information Services 7.5

Error Summary

HTTP Error 404.0 - Not Found

The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.

 

It seems that the IIS7 CRM Application pool had been modified to revert back to using the version 2 framework. To resolve:

1. Open IIS 7 Manager

2. Navigate to the CRMAppPool and double click on it to enter the Basic Settings.

3. Change the Framework version to be v4:

 

Posted on 2. November 2010

KB2439176 - for Dynamics CRM 2011 Beta 1

If you chose to use Windows Update for your installation of Dynamics CRM 2011 beta 1, recently you will have been prompted with the option to install KB2439176. This changes the version:

 

  • 5.0.9585.106 Beta 1
  • 5.0.9585.110 Beta 1 with KB2439176

 

There appears to be little or no background on what this update includes - I've had no problems so far.

You can see all updates here:

http://catalog.update.microsoft.com/v7/site/search.aspx?q=crm

 

Posted on 2. November 2010

You have already imported and upgraded this organization

Since importing CRM 4 customisations xml is not possible with Dynamics CRM 2011 Beta 1, using the import organisation wizard is the only option. During development, I’ve found that there is no way of importing and upgrading a CRM 4.0 organisation more multiple times. Once you import and upgrade a CRM 4 organisation, the OrganisationId is retained in the database (rather than a new one being assigned as for when you import a CRM 2011 organisation database), so importing and upgrading the same organisation results in the error "You have already imported and upgraded this organization". Even if you delete the previous import, the import still fails.

To get around this I used the following steps:

Warning: I'm doing this a development environment, and this is something that you would never do in production. Also, always make a backup first.

1. Disable and Delete the previously imported organisation using Deployment Manager

2. Run the following script against the MSCRM_Config database. Replace 'YourOrgName' with the name of the organisation you used when importing the first time.

-- Remove a previously imported and upgrade organisation
DECLARE @orgName nvarchar(255) ='YourOrgName' 
DECLARE @organisationid uniqueidentifier
SELECT @organisationid=ID from Organization WHERE UniqueName=@orgName
DELETE FROM SystemUserOrganizations where OrganizationId=@organisationid
DELETE FROM OrganizationProperties where ID=@organisationid
DELETE FROM OrganizationFeatureMap where OrganizationId=@organisationid
DELETE FROM OrganizationMaintenanceJobs where OrganizationId=@organisationid
DELETE FROM Organization where ID=@organisationid


 

 3. Re-import the organisation using Deployment Manager

 

Posted on 30. October 2010

Details of CRM 2011 Online Pricing

Things are hotting up for the up coming release of CRM 2011 online. Rumours of really competitive pricing structure have been confirmed to be ~£27/user for the first 12 months making it extreamly comettetive compared to salesforce.

Tatarinov today announced a new global launch promotion for Microsoft Dynamics CRM Online at a price of $34 (€31) per user, per month for the first 12 months of service; this pricing will be available to new customers from launch until June 30, 2011. This promotion complements a partner incentive that was announced in July 2010, where partners can receive 40 percent margin on the value of new subscriptions for Microsoft Dynamics CRM Online during the same time period. Formal launch events for Microsoft Dynamics CRM 2011 and the upcoming global release of Microsoft Dynamics CRM Online will begin in January 2011.

http://www.microsoft.com/presspass/press/2010/oct10/10-14convergenceeuropepr.mspx

 

This kind of pricing combined with the free trial that will be on offer will really lower barriers to entry for the SME market. CTO/CIO's will be signing up and becoming hooked on Dynamics CRM 2011's engaging functionality, market place and community. The adoption process can progress significantly without any road-blocks with internal IT resource.