Skip to content

Testability for Dynamics CRM SDK code Part 2

In Part 1 I showed how I code against the Dynamics CRM SDK with my own ICrmSession and how I can do unit-testing based on those few interfaces and classes. In this second part I’d like to cover some more topics the reader may find interesting.

Testing ExecutionRequests

When coding agains the Dynamics CRM SDK from outside of CRM (like for data import/update services) one likes to do multiple data updates within one atomic transaction. The only way I found to do is to build an ExecuteTransactionRequest and fill its Requests collection with CreateRequest and UpdateRequest instances. Then fire this ExecuteTransationRequest using OrganizationContext.Execute() method.

Here is some sample code I’ve built on top of my ICrmSession as discussed in my previous post. Its a simplified version of my code just to point out the basic principe how I send Insert’s and Update’s to Dynamics CRM. Code like this I have used in my base-class for Upsert-Services (one per Entity).

        public virtual OrganizationResponse DoUpsert(ICrmSession session, TUpsertData upsertData, string entityIdentification, IQueryable<TEntity> query, Func<TEntity> initNewEntity, Action<TEntity, TUpsertData> updateEntityFields)
        {
            if (session == null) { throw new ArgumentNullException(nameof(session)); }
            if (upsertData == null) { throw new ArgumentNullException(nameof(upsertData)); }

            var request = new ExecuteTransactionRequest
            {
                Requests = new OrganizationRequestCollection(),
                ReturnResponses = true,
            };

            // Update existing
            var entityExists = false;
            foreach (var existingEntity in query)
            {
                entityExists = true;
                ...
                updateEntityFields(updateEntity, upsertData);
                updateEntity.Id = existingEntity.Id;
                updateEntity.EntityState = EntityState.Changed;
                request.Requests.Add(new UpdateRequest { Target = updateEntity });
            }

            // Insert connection-object if no one did exist
            if (!entityExists)
            {
                var newEntity = initNewEntity();
                updateEntityFields(newEntity, upsertData);
                request.Requests.Add(new CreateRequest { Target = newEntity });
            }

            try
            {
                return session.OrganizationService.Execute(request);
            }
            catch (FaultException<OrganizationServiceFault> ex)
            {
                // TODO: Log the exception
                throw;
            }
        }

As discussed the main point is to build a ExecuteTransactionRequest containing CreateRequest and UpdateRequest instances. Then I fire it using session.OrganizationService.Execute(). The important thing here is that I use my ICrmSession to get the organization service.

With this code in place I can easily unit-test without the need for a real Dynamics CRM connection. To do so I can re-use my CrmSessionTestBase class I showed already in the previous post:

public abstract class CrmSessionTestBase
{
    public CrmSessionFake CrmSession { get; private set; }
    public IOrganizationService OrgService { get; private set; }

    [SetUp]
    public void SetUpCrmSession()
    {
        CrmSession = new CrmSessionFake();
    }

    [TearDown]
    public void TearDownCrmSession()
    {
        CrmSession?.Dispose();
        CrmSession = null;
    }
}

This unit-test baseclass creates a fake of ICrmSession which records the sent organization requests using a dynamic fake built with the Fake It Easy library. Here is the CrmSessionFake code. For more information read the previous post.

using System;
using System.Collections.Generic;
using FakeItEasy;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Client;

namespace CrmTests
{
    /// <summary>
    /// Fake of a <see cref="ICrmSession"/> that can be used for unit-testing.
    /// </summary>
    /// <seealso cref="ICrmSession" />
    public class CrmSessionFake : ICrmSession
    {
        public CrmSessionFake()
        {
            OrganizationService = SetUpOrganizationServiceFake();
            OrganizationServiceContext = SetUpOrganizationServiceContextFake();
        }

        /// <summary>
        /// Gets the <see cref="OrganizationRequest"/>'s that where sent using <see cref="OrganizationService"/>
        /// or <see cref="OrganizationServiceContext"/>.
        /// </summary>
        /// <value>The sent <see cref="OrganizationRequest"/>'s.</value>
        public IList<OrganizationRequest> SentOrganizationRequests { get; } = new List<OrganizationRequest>();

        /// <summary>
        /// Gets the organization service.
        /// </summary>
        /// <value>The organization service.</value>
        /// <seealso cref="IOrganizationService"/>
        public IOrganizationService OrganizationService { get; }

        /// <summary>
        /// Gets the typed organization service context.
        /// </summary>
        /// <value>The organization service context.</value>
        /// <seealso cref="ICrmSession.OrganizationServiceContext"/>
        public OrganizationServiceContext OrganizationServiceContext { get; }

        /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
        public void Dispose()
        {
        }

        IOrganizationService SetUpOrganizationServiceFake()
        {
            var result = A.Fake<IOrganizationService>();

            // Record all call OrganizationRequest's sent to .Execute() in SentOrganizationRequests
            A.CallTo(() => result.Execute(A<OrganizationRequest>._))
                .ReturnsLazily((OrganizationRequest request) =>
                {
                    SentOrganizationRequests.Add(request);
                    return new OrganizationResponse
                    {
                        Results = new ParameterCollection(),
                        ResponseName = Guid.NewGuid().ToString(),
                    };
                });

            return result;
        }

        OrganizationServiceContext SetUpOrganizationServiceContextFake()
        {
            var result = A.Fake<OrganizationServiceContext>();
            A.CallTo(result)
                .Where(call => call.Method.Name == "CreateQuery")
                .WithNonVoidReturnType()
                .CallsBaseMethod();

            return result;
        }
    }
}

The important point here is that each unit-test that inherits from CrmSessionTestBase has a property CrmSession which is of type ICrmSession (interface). It is set up with a fake so it records all call to OrganizationService.Execute() within the property SendOrganizationRequests for later use.

So my final unit-tests are as simple as this:

    [TestFixture]
    public class MyUpsertServiceTests : CrmSessionTestBase
    {
        [Test]
        public void Test_InsertNewEntity()
        {
            // Arrange
            var upsertData = CreateUpsertData();
            var service = new MyUpsertService();

            // Act
            var organizationResponse = service.Upsert(CrmSession, upsertData);

            // Assert
            ValidateEntity<CreateRequest>(organizationResponse, upsertData);
        }

        [Test]
        public void Test_UpdateExistingEntity()
        {
            // Arrange
            var upsertData = CreateUpsertData();

            // Call to OrganizationContext.CreateQuery() would return a fix (aka existing) is_contractaccount
            A.CallTo(CrmSession.OrganizationServiceContext)
                .Where(x => x.Method.Name == "CreateQuery")
                .WithReturnType<IQueryable<MyEntity>>()
                .ReturnsLazily(() => new List<MyEntity> { CreateEntity(upsertData) }.AsQueryable());

            var service = new MyUpsertService();

            // Act
            var organizationResponse = service.Upsert(CrmSession, upsertData);

            // Assert
            ValidateEntity<UpdateRequest>(organizationResponse, upsertData);
        }

Again, this is a simplified version of my code as I did a generic implementation of this for re-use with all of my Upsert-Services but it should show the important parts.

Two things to point out here:

The Test_UpdateExistingEntity() highjack the query execution by faking the result for calls to OrganizationServiceContext.CreateQuery(). This is done with Fake It Easy again. See here for more samples and a quickstart with Fake It Easy. I highly recommend getting used to a smart fake/mock library like Fake It Easy. It can help you so much for unit-testing.

The second thing is the ValidateEntity() method. I use it to check if the right requests would have been triggered by my upsert service. Means, I am checking if CreateRequest or UpdateRequest are triggered and with what entity data (upsertData). First my implementation which needs some Reflection hacking:

protected void ValidateEntity<TExpectedRequestType>(OrganizationResponse organizationResponse, TUpsertData upsertData)
    where TExpectedRequestType : OrganizationRequest
{
    organizationResponse.Should().NotBeNull();
    CrmSession.SentOrganizationRequests.Should().HaveCount(1);
    CrmSession.SentOrganizationRequests.First().Should().BeOfType<ExecuteTransactionRequest>();
        ((ExecuteTransactionRequest)CrmSession.SentOrganizationRequests.First()).Requests.Should().HaveCount(1);
    ((ExecuteTransactionRequest)CrmSession.SentOrganizationRequests.First()).Requests.First().Should().BeOfType<TExpectedRequestType>();

    var request = (TExpectedRequestType)((ExecuteTransactionRequest) CrmSession.SentOrganizationRequests.First()).Requests.First();

    // HACK: Get the entity out of the "Target" property
    var requestType = request.GetType();
    var propType = requestType.GetProperty("Target");
    var entity = (TEntity)propType.GetValue(request);

    ValidateEntityContent(upsertData, entity);
}

The first few lines validate and extract the requests from CrmSession.SentOrganizationRequests. No Rocket Science here.

Then the tricky part is to get the entity data out of either CreateRequest or UpdateRequest in a generic way. The problem here is, that both have a property called Target which holds the entity but sadly the Dynamics CRM SDK API is not very well crafted here. The base class of CreateRequest and UpdateRequest is the general purpose OrganizationRequest which does not have a Target property. There is no common base class like TargetedOrganizationRequests. The Microsoft Team just copy-pasted the Target property to both classes. Even worst, they made both classe sealed so they can not be “fixed” from outside outside by re-introduce at least a shared interface for the Target property.

Long story short: the only way I was able to address this using generic code (without using copy-paste) was to use the Hack with Reflection. Its a shame but better then copy-paste like Microsoft Dynamics did.

Updating and restoring data on Dynamics CRM during unit-tests

Sometimes the above faked CRM-connectivity are not enough. The problem is that you only test what you code will fire against Dynamics CRM but you don’t test the reaction of CRM. For example if Dynamics CRM for some reason don’t accept the data you send this would not be covered with the above kind of unit-tests.

To address these kind of scenarios I added some tests that do more of an integration-tests kind of unit-tests (black-box instead of white-box testing).

To do so I use a baseclass for my unit-tests that does not use CrmSessionFake but a real CrmSession instances. These tests then connect to the projects DEV-Instance of Dynamics CRM Server and do real data-update.

The problem here is again that the CRM SDK greatly laks of clean transaction support. There is no *begin-commit-rollback” transaction style API. So the common pattern to just open a database transaction before the testing code runs and then always rollback the transaction at the end of the unit-test doesn’t work here.

I address it in a more classic scrappy way by doing a try-finally and delete the data I’ve just created again. This requires a few things so it is at least a little bit stable:

  1. Each test-run should create its own unique set of data so the clean-up code can identify the data it needs to delete.

  2. Update of existing records can not be easily be undo (eg. if other testrun runs in parallel). So create your unique test-data before and then test the updating with the create test-data.

  3. Build re-usable helpers to clean up the data.

My pattern for these kind of tests goes like this:

// Generate testdata
var updateData = new MyData{ MyNumber = $"Test_{Random.Next(9999)}" };
GenerateUpdateData(updateData);

try
{
    // Run testcode which updates the real CRM data in here ...
}
finally
{
    // Clean up
    RemoveEntities(s => CreateQuery(s, updateData.MyNumber ));
}

And finally my little helper method RemoveEntities which I placed in my unit-tests baseclass:

protected void RemoveEntities<TEntity>(Func<ICrmSession, IQueryable<TEntity>> queryBuilder) where TEntity : Entity
{
    using (var session = SessionFactory.CreateSession())
    {
        foreach (var entity in queryBuilder(session))
        {
            session.OrganizationServiceContext.DeleteObject(entity, true);
        }

        session.OrganizationServiceContext.SaveChanges(SaveChangesOptions.ContinueOnError);
    }
}

It gets a ICrmSession from my session factory and calls the queryBuilder delegate (Lambda etc.) to get a query. Then it loads the results from the query and deletes each of them using OrganizationServiceContext.DeleteObject(). Finally the changes get saved using the SaveChanges() method.

Conclusion

If one spends a few hours once to introduce a few basic classes and interfaces things later get easy to work with. In my case having the ICrmSession and “SessionTestBase class opens up a whole lot of possibillities. So when starting with a new project take your time – you will easily save it later on.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: