CRM Plugin Unit Testing with Microsoft Fakes

Recently I got a chance to try my hands around writing unit tests for CRM Plugins. For this effort I used Microsoft Fakes framework. In order to ensure reusability of the code in each CRM plugin and help other developers to quickly adapt to writing unit tests for plugins, I came up with following classes. My vision is to use these classes as a framework for Plugin unit tests.

The classes are as below
1. ShimUtilityMethods – This is a wrapper class which provides shim methods for each utility method being used in the plugin. ShimUtilityMethods class definitions can be accessed at ShimUtilityMethods.cs.
2. ShimSDKMessages – This is a wrapper class which provides shim methods for each SDK messages (Create, Update, etc.) To bring more clarity to the user, I decided to separate out SDK message shim methods into a separate class. ShimSDKMessages class definitions can be accessed at ShimSDKMessages.cs.

How to use these in writing unit tests?

I will explain this with an example plugin. Consider a plugin which does attribute validation before the QualifyLead event. The plugin is registered at PreOperation event of the QualifyLead message. The plugin throws an exception terminating the process if any of the attribute is not present for qualification. Plugin code is as follows

public void Execute(IServiceProvider serviceProvider)
{
    try
{
        // Plugin Context code
 
// Get the Lead reference from the context
       EntityReference leadRef = (EntityReference)context.InputParameters[“LeadId”];
       // Retrieve Lead entity with all attributes
       lead = service.Retrieve(“lead”, leadRef.Id, new ColumnSet(true));
if (lead != null)
{
             //Validate required fields for qualification
ValidateRequiredFields(lead);
}
}/// <summary>
/// To validate required attributes of Lead for qualification
/// </summary>
/// <param name=”leadToValidate”>Lead</param>
private void ValidateRequiredFields(Entity leadToValidate)
{
       List<string> failedAttributes = new List<string>(); // stores the list of attributes which failed validation
       if (!leadToValidate.Attributes.Contains(“new_product”)) 
          failedAttributes.Add(“Product”);
       if (!leadToValidate.Attributes.Contains(“emailaddress1”))
          failedAttributes.Add(“Email”);
       if (!leadToValidate.Attributes.Contains(“address1_line1”)) 
          failedAttributes.Add(“Street 1”);
       if (!leadToValidate.Attributes.Contains(“numberofemployees”)) 
          failedAttributes.Add(“Number of Employees”);
       if (!leadToValidate.Attributes.Contains(“new_seats”))
          failedAttributes.Add(“Seats”);
       if (!leadToValidate.Attributes.Contains(“telephone1”))
failedAttributes.Add(“Mobile phone or Business Phone or Company Phone”);
if (failedAttributes.Count > 0)
{
           string failedAttributeList = null;
           for (int i = 0; i < failedAttributes.Count; i++)
{
failedAttributeList = failedAttributeList + failedAttributes[i];
               if (i != failedAttributes.Count – 1 && i != failedAttributes.Count – 2)
                  failedAttributeList = failedAttributeList + “, “;
               if (i == failedAttributes.Count – 2)
                  failedAttributeList = failedAttributeList + ” and “;
}
           // throw error and terminate the plugin execution
           throw new InvalidPluginExecutionException(“Following fields are required for the qualification of            Lead to an Opportunity – “ + failedAttributeList);
}
}}

 

Here’s a class which contains unit tests for the above plugin code. The class uses the 2 framework classes to write unit test code effectively.

[TestClass()]public class PreLeadQualificationTests

{

private static StubIServiceProvider fakeServiceProvider;

private static StubIOrganizationService fakeService;

[ClassInitialize]

public static void ClassInit(TestContext testContext)

{

fakeServiceProvider = ShimUtilityMethods.GetFakeServiceProvider();

var fakeContext = ShimUtilityMethods.GetFakePluginContext();

var fakeServiceFactory = ShimUtilityMethods.GetFakeServiceFactory(); // Fake Service Factory

fakeService = ShimUtilityMethods.GetFakeService();

}

enum TestScenario

{

ExecuteValidationExceptionTest1,

ExecuteValidationExceptionTest2

}

[TestMethod()]

public void ExecuteValidationExceptionTest2()

{

scenario = TestScenario.ExecuteValidationExceptionTest2;

using (ShimsContext.Create())

{

ShimUtilityMethods.ShimGetServiceType();

ShimUtilityMethods.ShimCreateOrganizationService();

ShimUtilityMethods.ShimInputParameters(ContextParameters);

ShimUtilityMethods.ShimPluginException();

ShimSDKMessages.ShimRetrieve(ValidateRequiredFieldsTestFewAttributes,fakeService);

var expectedMessage = string.Empty;

var actualMessage = string.Empty;

PreLeadQualification plugin = new PreLeadQualification();

plugin.Execute(fakeServiceProvider);

actualMessage = ShimUtilityMethods.actualMessage;

expectedMessage = “Following fields are required for the qualification of Lead to an Opportunity – Product family, Number of Employees and Estimated number of seats”;

Assert.AreEqual(expectedMessage, actualMessage);

}

}

private Entity ValidateRequiredFieldsTestAllAttributes(string entityName)

{

// Validate null values

Entity fakeLead1 = new Entity(entityName);

fakeLead1.Attributes[“mbs_productid”] = new EntityReference(“product”, Guid.Empty);

fakeLead1.Attributes[“emailaddress1”] = “noemail@thismail.com”;

fakeLead1.Attributes[“address1_line1”] = “line1”;

fakeLead1.Attributes[“numberofemployees”] = “500”;

fakeLead1.Attributes[“new_estimatednoofseats”] = “1000”;

fakeLead1.Attributes[“telephone1”] = “121231212”;

fakeLead1.Attributes[“companyname”] = “Test Company”;

fakeLead1.Attributes[“new_countryid”] = new EntityReference(“mbs_country”,Guid.Empty);

return fakeLead1;

}

}

The unit test class above uses the ShimUtilityMethods and ShimSDKMessages classes which provides shim methods for each service call made in the original plugin code. The call to those shim methods sets up the TestMethod with the required shims for a test scenario. Additionally,  the shim methods also gives an opportunity for the developer to define his own business logic to be returned when a particular shim method is executed.

For example, notice that ShimSDKMessages.ShimRetrieve method above requires 2 parameters, first parameter being a delegate Func<string,Entity> and the other being the fake service instance. This provides a shim for Retrieve SDK call.

Here the developer writing the unit test case is required to define a method matching the signature of the Func<> parameter which will be passed as a delegate to the ShimSDKMessages.ShimRetrive call and will be executed when the Retrieve call is hit in the original plugin code. In this way, based on the business logic of the plugin, the developer can define his own object to be returned for a retrieve call within his plugin code.

In the above code, ValidateRequiredFieldsTestFewAttributes is a method defined by the developer for his test scenario. Another plus point is the developer can return scenario-specific object using this method.

 

Advantages of using these classes

1. User/Developer writing unit tests doesn’t have to worry about writing the shim and stub methods for different functions/service calls in the plugin code. All necessary shim methods will be a part of ShimUtilityMethods and ShimSDKMessages classes. Developer can focus on core business logic in the plugin.
2. User/Developer can define functions which return exact objects based on their plugin business logic requirements. The shim methods defined in the helper classes executes these functions and return appropriate object whenever they are called in the original plugin code.

Advertisement

ShimSDKMessages.cs

public static class ShimSDKMessages

{

public static Dictionary<string, object> actualUpdatedAttributes;

public static OrganizationServiceFault orgFault;

public static Func<string, Entity> ReturnTestEntityObject;

public static Func<ParameterCollection> ReturnInputParameterCollectionObject;

public static Func<ParameterCollection> ReturnOutputParameterCollectionObject;

public static Func<string, EntityCollection> ReturnEntityCollectionObject;

public static Func<string, DataCollection<Entity>> ReturnExecuteResponse;

/// <summary>

/// Fake for Create method

/// </summary>

public static void ShimCreate(StubIOrganizationService fakeService)

{

fakeService.CreateEntity = (entity) =>

{

switch (entity.LogicalName)

{

case “lead”:

case “account”:

case “opportunity”:

case “contact”:

return new Guid(“123456-1234-1234-1234-123456789012”);

default:

throw new NotImplementedException();

}

};

}

/// <summary>

/// Fake for Update

/// </summary>

public static void ShimUpdate(StubIOrganizationService fakeService)

{

fakeService.UpdateEntity = (entity) =>

{

switch (entity.LogicalName)

{

case “lead”:

case “account”:

case “opportunity”:

actualUpdatedAttributes = GetUpdatedValues(entity);

break;

default:

throw new NotImplementedException();

}

};

}

private static Dictionary<string, object> GetUpdatedValues(Entity entity)

{

AttributeCollection attCollection = entity.Attributes;

Dictionary<string, object> updatedValues = new Dictionary<string, object>();

updatedValues = actualUpdatedAttributes;

foreach (var attribute in attCollection)

{

updatedValues.Add(attribute.Key, attribute.Value);

}

return updatedValues;

}

/// <summary>

/// Fake for Query Experession failure

/// </summary>

public static void ShimQueryExpFail()

{

ShimQueryExpression.AllInstances.EntityNameGet = (queryexpression) =>

{

return queryexpression.EntityName = “Entity”;

};

}

/// <summary>

/// Fake for Retrieve

/// </summary>

public static void ShimRetrieve(Func<string, Entity> GetEntityObject, StubIOrganizationService fakeService)

{

ReturnTestEntityObject = GetEntityObject;

fakeService.RetrieveStringGuidColumnSet = (entityName, guid, columnSet) =>

{

return ReturnTestEntityObject(entityName);

};

}

/// <summary>

/// Fake for RetriveMultiple

/// </summary>

public static void ShimRetrieveMultiple(Func<string, EntityCollection> GetEntityCollection, StubIOrganizationService fakeService)

{

ReturnEntityCollectionObject = GetEntityCollection;

fakeService.RetrieveMultipleQueryBase = (fakeQueryBase) =>

{

QueryExpression fakeQuery = (QueryExpression)fakeQueryBase;

EntityCollection fakeCollection = null;

switch (fakeQuery.EntityName)

{

case “account”:

fakeCollection = new EntityCollection();

fakeCollection = ReturnEntityCollectionObject(“account”);

break;

case “contact”:

fakeCollection = new EntityCollection();

fakeCollection = ReturnEntityCollectionObject(“contact”);

break;

case “opportunityproduct”:

fakeCollection = new EntityCollection();

fakeCollection = ReturnEntityCollectionObject(“opportunityproduct”);

break;

default:

throw new FaultException<OrganizationServiceFault>(orgFault);

}

return fakeCollection;

};

}

}

ShimUtilityMethods.cs

public static class ShimUtilityMethods
{

public static StubIServiceProvider fakeServiceProvider = new StubIServiceProvider(); //Fake Service Provider;

public static StubIOrganizationServiceFactory fakeServiceFactory = new StubIOrganizationServiceFactory(); //Fake Service Provider;

public static StubIPluginExecutionContext fakePluginContext = new StubIPluginExecutionContext(); //Fake Service Provider;

public static StubIOrganizationService fakeService = new StubIOrganizationService(); //Fake Service Provider;

public static Func<ParameterCollection> ReturnInputParameterCollectionObject;

public static Func<ParameterCollection> ReturnOutputParameterCollectionObject;

public static Func<string> ReturnMessageName;

public static Func<EntityImageCollection> ReturnPostEntityImage;

public static Func<EntityImageCollection> ReturnPreEntityImage;

public static string actualEventLogMessage;

public static string actualEventLogSource;

public static string actualMessage;

/// <summary>

/// Fake for Service Provider

/// </summary>

/// <returns></returns>

public static StubIServiceProvider GetFakeServiceProvider()

{

return fakeServiceProvider;

}

/// <summary>

/// Fake for Plugin Context

/// </summary>

/// <returns></returns>

public static StubIPluginExecutionContext GetFakePluginContext()

{

return fakePluginContext;

}

/// <summary>

/// Fake for Service Factory

/// </summary>

/// <returns></returns>

public static StubIOrganizationServiceFactory GetFakeServiceFactory()

{

return fakeServiceFactory;

}

/// <summary>

/// Fake for Organization Service

/// </summary>

/// <returns></returns>

public static StubIOrganizationService GetFakeService()

{

return fakeService;

}

/// <summary>

/// Fake for GetServiceType method

/// </summary>

public static void ShimGetServiceType()

{

fakeServiceProvider.GetServiceType = (type) =>

{

if (type == typeof(IPluginExecutionContext))

{

return fakePluginContext;

}

else if (type == typeof(IOrganizationServiceFactory))

{

return fakeServiceFactory;

}

else

return null;

};

}

/// <summary>

/// Fake for CreateOrganizationService method

/// </summary>

public static void ShimCreateOrganizationService() // Called within GetFakeServiceFactory

{

// Shim CreateOrganizationService Method

fakeServiceFactory.CreateOrganizationServiceNullableOfGuid = (contextGuid) =>

{

if (contextGuid == Guid.Empty)

return fakeService;

else

return null;

};

}

/// <summary>

/// Fake for Context Message

/// </summary>

public static void ShimContextMessageName(Func<string> GetContextMessageName)

{

ReturnMessageName = GetContextMessageName;

using (ShimsContext.Create())

{

fakePluginContext.MessageNameGet = () =>

{

return ReturnMessageName();

};

}

}

/// <summary>

/// Fake for Get Input Parameters

/// </summary>

public static void ShimInputParameters(Func<ParameterCollection> GetInputParameters)

{

//Add values to the fakeContext

ReturnInputParameterCollectionObject = GetInputParameters;

fakePluginContext.InputParametersGet = () =>

{

return ReturnInputParameterCollectionObject();

};

}

/// <summary>

/// Fake for Event Log Entry

/// </summary>

public static void ShimEventLogEntry()

{

ShimEventLog.WriteEntryStringStringEventLogEntryType = (source, message, type) =>

{

actualEventLogMessage = message;

actualEventLogSource = source;

};

}

}

Auto-Save feature in CRM 2013

CRM 2013 introduced an Auto-save feature to automatically save the contents of the entity form. By default, auto-save is set to be triggered after every 30 seconds. This setting can be found in the MSCRM_CONFIG database using the below query.

select IntColumn from [dbo].[DeploymentProperties] where ColumnName=‘AutoSaveInterval’

Disabling the Auto-save feature

Auto-save feature can be disabled at an Organization level. To disable Auto-save,

Go to Settings -> Administration -> System Settings and set the Enable auto save on all forms to NO as in the image below

Disable Auto-save in CRM 2013

Disable Auto-save in CRM 2013

This disables the auto-save functionality across all the entities in the Organization.

In order to disable auto-save functionality only for specific entity based on the requirement, we can write the below Javascript function and call it on the OnSave event of that entity by passing the execution context to this function as an input parameter.

function preventAutoSave(execContext){
var eventArgs = execContext.getEventArgs();
if(eventArgs.getSaveMode() == 70) //save mode for auto-save = 70
        eventArgs.preventDefault();
}

Hiding Duplicate Warning dialog box during Lead Qualification Process in CRM 2013

Lead Qualification process in CRM 2013 converts the Lead record to an Opportunity and even creates the Account and the Contact record based on the information from the Lead record. During this process, before creating the related Account and Contact records, the product (CRM 2013) aptly does a duplicate check for Account and Contact based on the published Duplicate detection rules. If it finds a duplicate record for Account or Contact, it throws up the below Duplicate Warning dialog  box.

1

Problem

The problem with this dialog box is that it doesn’t come with an Account and/or Contact lookup field filled up with the duplicate record found as a result of the duplicate detection check. The end user is required to provide a value for the lookup fields and click Continue to complete the qualification process.

Pitfalls of this approach 

1. End user qualifying a lead may not know the correct account and the correct contact record he needs to fill in the lookup.

2. It is not clear which record is duplicate – Account, Contact or both

3. As the lookup fields are editable, there is a high risk of the end user selecting incorrect records and causing incorrect Account – Lead and Contact – Lead associations within the system

4. End user wont like this manual intervention which confuses them more than helping them out

Solution

There is an easy way to hide this Duplicate Warning dialog box and still achieve the same functionality. This will require us to write our own code for checking the duplicate Account and Contact records.

This can be achieved in 2 steps.

1. Modify plugin context for SuppressDuplicateDetection

In the PreValidation plugin on the QualifyLead message for the Lead entity, we can modify the context of the plugin to suppress the duplicate detection check like below

// Step 1 : Set SuppresDuplicateDetection in context to True
context.InputParameters["SuppressDuplicateDetection"] = true// set to false by default 

By suppressing the duplicate detection, the lead qualification process will ignore the duplicate check for Account and Contact records and thus the dialog box wont show up.

2. Writing custom duplicate detection for Account and Contact records

For this we will be required to do write a custom code for detecting duplicates for Account and Contact and then update the Lead record with the duplicate records found. parentaccountid and parentcontactid attribute of lead will be set to account and contact record respectively. Here’s how we can do it in code

// Step 2: Check for Account  & Contact duplicates
Entity lead = newEntity("lead");
bool updateLead = false;
Guid account = CheckDuplicateAccount(lead);  // Check if account is already in CRM.
if (account != Guid.Empty)
{
lead.Attributes["parentaccountid"] = new EntityReference("account", account); // set parentaccountid on lead to the account found
}
Guid contact = CheckDuplicateContact(lead);    // Check if contact is already in CRM
if (contact != Guid.Empty)
{
lead.Attributes["parentcontactid"] = new EntityReference("contact", contact); // set parentcontactid on lead to the contact found
updateLead = true;
}
if(updateLead)                      // Update the lead record with the Account and Contact match found in CRM
{
service.Update(lead);
}

By performing the above two steps in PreValidation plugin, we are finding duplicates for Account and Contact and correctly associating them to the Lead record before the Lead qualification process kicks in. And by suppressing the duplicate detection, we ignore the OOB duplicate check thus hiding the unnecessary duplicate warning dialog box.

Controlling the Lead Qualification process in CRM 2013

CRM 2013 (Orion) removed the Convert Lead dialog box from the Lead Qualification process. While migrating to the Orion instance, this is becoming a roadblock for us as the product doesn’t give the option of controlling which entity records to create upon Lead qualification.

Orion directly creates all the three entity records – Account, Contact and Opportunity as a part of lead qualification process, thus removing the flexibility from the user of selecting the entity records that needs be created. The product however checks for possible duplicates for Account and Contact records but the process isn’t favored by many and the risk of creating duplicate records or associating lead and opportunity to incorrect Account/Contact cannot be neglected.

I mentioned it as a roadblock for us because we have several business rules on lead qualification process wherein we don’t create Opportunity and Account record for a couple of qualification reasons.

In order to implement the same business rules in CRM 2013 Orion, I was researching a way to restrict the create of Opportunity/Account record for specific lead qualification status reason.

Here’s a simple way to do it –

Write a PreValidation plugin on the QualifyLead message for the Lead entity. Based on a certain condition, change the plugin context as follows

// Requirement – if the lead qualification reason is 100000021, do not create account and opportunity records
if (((OptionSetValue)(context.InputParameters["Status"])).Value == 100000021) // status is the lead qualification reason selected by the user
{
     context.InputParameters["CreateOpportunity"] = false; // set to true by default
      context.InputParameters["CreateAccount"] = false; // set to true by default
}

The InputParameters collection in the plugin context contains the key-value pairs for Account (CreateAccount), Contact (CreateContact) and Opportunity (CreateOpportunity). As shown in the above code, we can change these values to false under specific condition and hence restrict/control the unnecessary record creation.