My experience writing unit tests with FakeXRMEasy

Hello;

I thoroughly enjoy writing unit tests with FakeXRMEasy. It’s a framework that allows you to fake the Dynamics 365 services into the proper contexts and then use the messages against it. It can be found at https://dynamicsvalue.com/.

The positives I’ve found from using this framework are:

  • I’m able to debug the custom code without ever having to leave Visual Studio.
    • No more spending time getting plugins to debug properly through visual studio
  • I’m able to replicate a wide variety of scenarios through data in my unit tests without having to step through everyone within the Dynamics 365 platform itself.
  • Works with custom workflow activities, plugins, portals, javascript.

Let’s walk through a piece of code. The scenario is we are building a sports application in Dynamics 365; when a game ends a plugin triggers. This plugin updates the winning team with the win as well a point total; then adds a loss to the losing team. Let’s examine the plugin first.

        public void Execute(IServiceProvider serviceProvider)
        {
            
            
            if(serviceProvider ==null)
            {
                throw new ArgumentNullException("serviceProvider", "serviceProvider cannot be a null refrence");
            }

            IPluginExecutionContext context = (IPluginExecutionContext)(serviceProvider.GetService(typeof(IPluginExecutionContext)));

            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

            //Lets get the game information and update each teams record
            Entity target = (Entity)context.InputParameters["Target"];

            try
            {
                Entity aGame = null;

                tracingService.Trace("Let's get the game entity with all of it's columns. The ID is:" + target.Id.ToString());
                if(context.MessageName== "Update")
                {
                    aGame = (Entity)service.Retrieve(target.LogicalName, target.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true));                
                }
                else
                {
                    throw new Exception("Plugin:EndGame - This plugin should only be executed against an update message not a create or delete.");
                }

                //Let's validate that it returned something
                if(aGame != null)
                {
                    EntityReference homeTeam = null;
                    if(aGame.Attributes.Contains("jmvp_hometeam"))
                    {
                        homeTeam = (EntityReference)aGame.Attributes["jmvp_hometeam"];
                    }

                    EntityReference awayTeam = null;
                    if (aGame.Attributes.Contains("jmvp_awayteam"))
                    {
                        awayTeam = (EntityReference)aGame.Attributes["jmvp_awayteam"];
                    }

                    OptionSetValue gameOutcome = null;
                    if (aGame.Attributes.Contains("jmvp_outcome"))
                    {
                        gameOutcome = (OptionSetValue)aGame.Attributes["jmvp_outcome"];
                        //Home - 492,470,000
                        //Away - 492,470,001
                        //Tie  - 492,470,002
                    }

                    Entity homeTeamEntity = getEntitywithFields(homeTeam, true, service);

                    Entity awayTeamEntity = getEntitywithFields(awayTeam, true, service);

                    switch(gameOutcome.Value)
                    {
                        //Home
                        case 492470000:
                            homeTeamEntity.Attributes["jmvp_winrecord"] = (int)homeTeamEntity.Attributes["jmvp_winrecord"] + 1;
                            homeTeamEntity.Attributes["jmvp_totalpoints"] = (int)homeTeamEntity.Attributes["jmvp_totalpoints"] + 2;
                            awayTeamEntity.Attributes["jmvp_lossrecord"] = (int)awayTeamEntity.Attributes["jmvp_lossrecord"] + 1;
                            service.Update(homeTeamEntity);
                            service.Update(awayTeamEntity);
                            break;
                        //Away
                        case 492470001:
                            awayTeamEntity.Attributes["jmvp_winrecord"] = (int)awayTeamEntity.Attributes["jmvp_winrecord"] + 1;
                            awayTeamEntity.Attributes["jmvp_totalpoints"] = (int)awayTeamEntity.Attributes["jmvp_totalpoints"] + 2;
                            homeTeamEntity.Attributes["jmvp_lossrecord"] = (int)homeTeamEntity.Attributes["jmvp_lossrecord"] + 1;

                            service.Update(awayTeamEntity);
                            service.Update(homeTeamEntity);
                            break;
                        //Tie
                        case 492470002:
                            break;   



                    }


                }



            }
            catch(Exception ex)
            {
                throw (ex);
            }

        }

        private Entity getEntitywithFields(EntityReference entityToRetrieve, Boolean columnSet, IOrganizationService service)
        {

            return service.Retrieve(entityToRetrieve.LogicalName, entityToRetrieve.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(columnSet));


        }
    }

As you can see it simple does a check on the jmvp_outcome field to determine who won and updates appropriately. Now using FakeXRMEasy let’s walk through two separate unit tests. The first one is where the home team wins the game.

[TestMethod()]
        public void HomeTeamWins()
        {
            

            var fakedContext = new XrmFakedContext();

            var team1 = new Entity("jmvp_sportsteam");
            team1.Id = Guid.NewGuid();
            team1["jmvp_name"] = "Canadiens";
            team1["jmvp_winrecord"] = 10;
            team1["jmvp_lossrecord"] = 8;
            team1["jmvp_totalpoints"] = 20;

            var team2 = new Entity("jmvp_sportsteam");
            team2.Id = Guid.NewGuid();
            team2["jmvp_name"] = "Maple Leafs";
            team2["jmvp_winrecord"] = 12;
            team2["jmvp_lossrecord"] = 6;
            team2["jmvp_totalpoints"] = 24;

            var game1 = new Entity("jvmp_game");
            game1.Id = Guid.NewGuid();
            game1["jvmp_name"] = "November 5th - Canadiens vs Maple Leafs";
            game1["jmvp_hometeam"] = new EntityReference(team1.LogicalName, team1.Id);
            game1["jmvp_awayteam"] = new EntityReference(team2.LogicalName, team2.Id);
            game1["jmvp_outcome"] = new OptionSetValue(492470000);

            fakedContext.Initialize(new List<Entity>()
            {
                team1, team2, game1, game2
            });


            

            ParameterCollection inputParameters = new ParameterCollection();
            inputParameters.Add("Target", game1);

            var plugCtx = fakedContext.GetDefaultPluginContext();
            plugCtx.MessageName = "Update";
            plugCtx.InputParameters = inputParameters;
            plugCtx.Depth = 1;

            var FakedPlugin = fakedContext.ExecutePluginWith<EndGame>(plugCtx);

            IOrganizationService service = fakedContext.GetOrganizationService();

            Entity updatedHomeTeam = service.Retrieve(team1.LogicalName, team1.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true));

            Entity updatedAwayTeam = service.Retrieve(team2.LogicalName, team2.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true));

            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team1["jmvp_winrecord"] + 1), (int)updatedHomeTeam["jmvp_winrecord"]);
            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team1["jmvp_totalpoints"] + 2), (int)updatedHomeTeam["jmvp_totalpoints"]);
            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team2["jmvp_lossrecord"] + 1), (int)updatedAwayTeam["jmvp_lossrecord"]);


        }

Let’s dive a bit deeper into this unit test. First we create the entities and data set. Then we load it into the faked context “XrmFakedContext”.

            var fakedContext = new XrmFakedContext();

            var team1 = new Entity("jmvp_sportsteam");
            team1.Id = Guid.NewGuid();
            team1["jmvp_name"] = "Canadiens";
            team1["jmvp_winrecord"] = 10;
            team1["jmvp_lossrecord"] = 8;
            team1["jmvp_totalpoints"] = 20;

            var team2 = new Entity("jmvp_sportsteam");
            team2.Id = Guid.NewGuid();
            team2["jmvp_name"] = "Maple Leafs";
            team2["jmvp_winrecord"] = 12;
            team2["jmvp_lossrecord"] = 6;
            team2["jmvp_totalpoints"] = 24;

            var game1 = new Entity("jvmp_game");
            game1.Id = Guid.NewGuid();
            game1["jvmp_name"] = "November 5th - Canadiens vs Maple Leafs";
            game1["jmvp_hometeam"] = new EntityReference(team1.LogicalName, team1.Id);
            game1["jmvp_awayteam"] = new EntityReference(team2.LogicalName, team2.Id);
            game1["jmvp_outcome"] = new OptionSetValue(492470000);

            fakedContext.Initialize(new List<Entity>()
            {
                team1, team2, game1, game2
            });

Once we’ve  initialized our fakedContext we’re ready to setup the input parameters and define some information around this plugin. When setting up the plugin information we’re able to select the message name which allows us to handle all of messages; which means we can unit test against any of the events. We set the entity that’s trigger the plugin and how much data we want to pass into it. Then i’m setting the depth of the plugin which can be important. The last thing that needs to be done is to actually execute the plugin itself.

            ParameterCollection inputParameters = new ParameterCollection();
            inputParameters.Add("Target", game1);

            var plugCtx = fakedContext.GetDefaultPluginContext();
            plugCtx.MessageName = "Update";
            plugCtx.InputParameters = inputParameters;
            plugCtx.Depth = 1;

            var FakedPlugin = fakedContext.ExecutePluginWith<EndGame>(plugCtx);

The last thing that I do is to validate my expected outcomes. In this case I want to ensure team 1’s properly got the additional points and win record. As well team 2’s loss total has increased by one.

            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team1["jmvp_winrecord"] + 1), (int)updatedHomeTeam["jmvp_winrecord"]);
            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team1["jmvp_totalpoints"] + 2), (int)updatedHomeTeam["jmvp_totalpoints"]);
            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team2["jmvp_lossrecord"] + 1), (int)updatedAwayTeam["jmvp_lossrecord"]);

Although this is a very simple example of a unit test; I think it gives a good example of whats within the capability of mocking up the fakedContext.

Hope this helps!

What is a system entity message?

Hello;

With the release of V9 we’ve seen new items in the solution structure and package. One such change is the Messages under each entity in the solution.

 

Messages give the ability to change the content displayed when the user sees these out of the box messages. It includes user interface text and error messages. It gives you the default display string and allows you to enter a custom display string and then a comment to explain the change. Editing is very straight forward and looks like this:

It’s worth noting that entity messages aren’t available for custom entities and that your only able to edit existing messages.

Managed Solutions V9 thoughts

The great debate between unmanaged and managed solutions rages on. This discussion started when solutions was first introduced in Dynamics CRM 2011. Many people go the route of unmanaged being burned by managed; each person with their own battle scars.

Personally I’ve been burned by unmanaged which makes on the managed side of the fence. It hasn’t been easy along the journey of these solutions but with the changes introduced in Dynamics CRM 2016 around patching and being able to actually remove technical debt or depreciated fields from a managed solution made the viability much higher in my opinion.

It’s worth considering Microsoft’s view point of managed solutions in every environment past development organizations. It can be easily viewed in the new v9 solution changes happening at the platform level. As the CRM product family has been expanding over the past few years we’ve seen the sales & service & marketing split into separately SKUs and available individually now. This has been done through managed solutions and Microsoft’s ability to layer them properly.

As an example of this I simply created a new solution and added the out of the box Account entity and exported it as unmanaged. When looking at the solution file it shows every dependency for the account entity and what solutions it comes from. A few sample are:

     <MissingDependency>
        <Required key="26" type="1" schemaName="territory" displayName="Territory" solution="msdynce_AppCommon (9.0.4.0022)" />
        <Dependent key="27" type="10" schemaName="msdyn_territory_account_ServiceTerritory" displayName="msdyn_territory_account_ServiceTerritory" parentSchemaName="account" parentDisplayName="Account" />
      </MissingDependency>
      <MissingDependency>
        <Required key="28" type="1" schemaName="bookableresource" displayName="Bookable Resource" solution="msdynce_Scheduling (9.0.0.0)" />
        <Dependent key="29" type="10" schemaName="msdyn_bookableresource_account_PreferredResource" displayName="msdyn_bookableresource_account_PreferredResource" parentSchemaName="account" parentDisplayName="Account" />
      </MissingDependency>
      <MissingDependency>
        <Required key="30" type="1" schemaName="msdyn_taxcode" displayName="Tax Code" solution="FieldService (7.4.0.74)" />
        <Dependent key="31" type="10" schemaName="msdyn_msdyn_taxcode_account_SalesTaxCode" displayName="msdyn_msdyn_taxcode_account_SalesTaxCode" parentSchemaName="account" parentDisplayName="Account" />
      </MissingDependency>
      <MissingDependency>
        <Required key="32" type="2" schemaName="name" displayName="Territory Name" parentSchemaName="territory" parentDisplayName="Territory" solution="msdynce_AppCommon (9.0.4.0022)" />
        <Dependent key="27" type="10" schemaName="msdyn_territory_account_ServiceTerritory" displayName="msdyn_territory_account_ServiceTerritory" parentSchemaName="account" parentDisplayName="Account" />
      </MissingDependency>

This gives us a bit of an inside view on solutions being used by Microsoft like “msdynce_Scheduling (9.0.0.0)” and “msdynce_AppCommon (9.0.4.0022)”. It’s really interesting how we’re able to see how they are splitting up the platform to enable different product offerings. It also shows the complexities in doing this with all of the out of the box entities. Needless to say V9 seems to be the first time we’re able to look around a bit more and understand where the platform team is coming from.

Hope you found this interesting because I sure do.

Creating an action using a custom workflow activity

Hello;

Today we’re going to be starting a series that will lead us into PowerApps. This is part one and it’s walks through creating a custom workflow activity, then inserting into a custom action in a v9 environment.

The business requirement for this post is to have an action that is able to activate/deactivate a process.

Custom Workflow Activity

Microsoft provides a very extensively list of out of the box actions which gets updated frequently. It’s worth checking out this documentation from time to time to catch the latest and greatest https://msdn.microsoft.com/en-us/library/mt607829.aspx. Unfortunately there isn’t an action to activate/deactivate a process; this has to be handled through a custom workflow activity. The following code below is what’s required.

    public class SetProcessState : CodeActivity

    {

        /// <summary>
        /// This is the workflow that needs to be activated/deactivated.
        /// </summary>
        [Input("Workflow")]
        [ReferenceTarget("workflow")]
        public InArgument<EntityReference> Workflow { get; set; }
        
        /// <summary>
        /// This is the state of the workflow entity.
        /// 0 is draft and 1 is Activated
        /// </summary>
        [Input("Workflow State")]
        public InArgument<int> WorkflowState { get; set; }

        /// <summary>
        /// This is the status of the workflow entity.
        /// </summary>
        [Input("Workflow Status")]
        public InArgument<int> WorkflowStatus { get; set; }

        protected override void Execute(CodeActivityContext executionContext)
        {
            //Create the tracing service
            ITracingService tracingService = executionContext.GetExtension<ITracingService>();

            //Create the context
            IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
            IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

            EntityReference workflow = Workflow.Get<EntityReference>(executionContext);
            int workflowState = WorkflowState.Get<int>(executionContext);
            int workflowStatus = WorkflowStatus.Get<int>(executionContext);

            if(workflow == null)
            {
                tracingService.Trace("The workflow entity reference past in is not valid");
                throw new Exception("The workflow entity reference past in is not valid");
            }

            tracingService.Trace("Activate/Deactivate Workflow:");

            var activateRequest = new SetStateRequest
            {
                EntityMoniker = new EntityReference
                    (workflow.LogicalName, workflow.Id),
                State = new OptionSetValue(workflowState),
                Status = new OptionSetValue(workflowStatus)
            };
            service.Execute(activateRequest);
            Console.WriteLine("and sent request to service.");

        }
    }

This custom workflow activity takes three parameters:

  1. Workflow – This is a entity reference to the actual process that’s going to be activated/deactivated.
  2. Workflow State – This is the state of the workflow entity
  3. Workflow Status – This is the status of the workflow entity.

With these three parameters we’re ready to create the action which will allow us to activate/deactivate a process.

Creating the Action

Creating an action is pretty straight forward; an action is a type of a process. The best part about actions is they become a message that’s callable through the web api directly. This alone makes them extremely powerful. It’s a great way to compartmentalize business logic or technical components.

Let’s navigate to a new solution I’ve setup where we are going to create an action.

This particular action is going to be a global action which means it can be called standalone as apposed to against a specific entity. I do this because the process entity isn’t in the list of entities. The first thing we’ll do once the action is created is to add the three actions we defined in the custom workflow activity.

Now we have the arguments that we can pass directly into the custom workflow activity allowing us to activate/deactivate any process. Let’s go ahead and add the call to our custom workflow activity and set the parameters of the step.

I wanted to stop and highlight when trying to set the Workflow value. All non entity reference arguments of the action are contained within the Local Values -> Arguments section shown above. The entity reference argument is shown differently as per the highlight item; it’s displayed under a section called Argument Entity. This is the one we’ll select to give the workflow a value. We can save and close.

We are now left with a completed action with the functionality to activate/deactivate processes. I hope you enjoyed the walk through on combining these two items together. Next post we’ll look at putting this action to good use through the API.