Interaction Testing

What vs. How

FakeXrmEasy is a mocking framework which is designed with a state-based design in mind. You compare the state of the in-memory database before and after the test execution. This certainly helps in testing a variety of scenarios as the Dataverse / CRM is very much data-driven. Testing using state focuses on What should happen, as opposed to How it should happen.

As soon as the requirements, and therefore the codebase, grows in complexity though, so does the data needed to test it. You’ll find yourself in situations where some functionality could be encapsulated and reused across a number of different scenarios. In that case you might want to refactor that logic and their associated unit tests into a dedicated class and interface without duplicating test data.

For example, when a large piece of functionality (A) needs to use some other piece of work (B), it’ll just need to make sure that is called with the correct parameters, because the responsibility of what should happen in that other work (B) should be given to that other piece of work itself (B), not to the main large work (A), otherwise we might end up with some fragile, over-specified tests where (A) would also test how (B) should behave, but that’s not (A)’s responsibility!

This will ensure we keep (A)’s test data to a minimum without duplicate (B)’s test data (and behavior) across a number of different scenarios.

Interaction testing so, refers to the “interaction” of the different pieces of logic, like (B), that are used in a larger work, like (A).

Using this pattern you could break down any functionality, no matter how big or complex, into smaller, reusable pieces of code, that can be easily tested, modified, or even removed, if that’s eventually the case.

Interaction testing could be implemented in many different ways, here we’ll be covering one inspired on Command Query Separation that is simple enough to start with.

The Command Design pattern

As the codebase grows, you might find yourself slowly moving away from basic CRUD operations, to applying some application architectures like explained on the CQS and CQRS patterns.

Here we are introducing the Command design pattern, which is also used in Command Query Separation above, and we’ll make it even simpler: we’re going to reuse a single command class that we’ll use for both the Commands and the Queries.

However, we won’t be mixing these in the one method of course, instead, we could distinguish if a method is a command or a query as long as it has a meaningful name.

For example, take a look at these 2 class names: SubmitNewIncidentCommand and GetAllOpenIncidentsCommand. These class names are descriptive enough to rather easily see that SubmitNewIncidentCommand is something that will likely update the state of the system (therefore it’s a command), whereas GetAllOpenIncidentsCommand is likely a query that returns a set of open incidents, without side effects.

You could use a convention like Get*Command for Queries and !Get*Command for Commands: basically anything that begins with “Get” is a query, otherwise it’ll be a command. Or even you could use different namespaces to group them, ie:

|--- Incidents
         |----- Commands
                   |------ SubmitNewIncidentCommand.cs
         |----- Queries
                   |------ GetAllOpenIncidentsCommand.cs

By using an approach like this, we’re focusing on the behavior of the application: all commands are pretty much stateless, as the state is managed in the Dataverse, and, when unit testing them, the state will be managed by FakeXrmEasy, simulated in an In-Memory database.

The IOrgServiceCommand interface

This interface is fairly simple, it essentially exposes 2 methods:

  • CanExecute : determines if all the conditions are met before executing the logic underneath, otherwise an error will be returned. For example, if the action is associated with, say, a button in the UI in a layer above, this would allow to perform certain checks, to say, enable / disable that button, for example.

  • Execute : executes the actual logic underneath

All our of commands will implement that interface. Because the methods return the same GenericResult class to return responses (which can be further extended), it’ll be extremely easy to do interaction testing as you’ll see.

/// A command interface that performs an action using an IOrganizationService underneath

public interface IOrgServiceCommand
{
    GenericResult CanExecute();
    GenericResult Execute();
}

The OrgServiceCommand base class

This is a base class that implements the above interface from which all the other commands will inherit. By using a base command class, we could reuse some common logic across all the commands in the entire application, such as, logging, exception handling, and so on…

Our actual commands will inherit from that base class and will just need to implement the ConcreteExecute (and the ConcreteCanExecute, if needed) methods.

public class OrgServiceCommand: IOrgServiceCommand
{
    protected readonly IOrganizationService _orgService;

    public OrgServiceCommand(IOrganizationService orgService)
    {
        _orgService = orgService;
    }

    public GenericResult Execute()
    {
        try 
        {
            var result = ConcreteCanExecute();
            if(result.Succeeded)
            {
                result = ConcreteExecute();
            }

            return result;
        }
        catch (Exception ex)
        {
            // Handling exceptions at this level across all the commands in the application provides a central place for logging and handling of 
            // these exceptions, which is very powerful

            // You could decide to show or hide exception details depending on different configurations too, for example
            return GenericResult.FailWith("Unknown error");
        }
    }

    public GenericResult CanExecute()
    {
        try 
        {
            return ConcreteCanExecute();
        }
        catch (Exception ex)
        {
            return GenericResult.FailWith("Unknown error");
        }
    }

    protected virtual GenericResult ConcreteCanExecute()
    {
        return GenericResult.Succeed();
    }

    protected virtual GenericResult ConcreteExecute()
    {
        throw new NotImplementedException("Must be inherited and overriden");
    }   
}

The GenericResult class

This class allows encapsulating the responses that will be used for all the interactions. There is a subclass of the GenericResult that allows returning pretty much any kind of reponse using generics.

Using the same return class will simplify quite a lot managing interactions as well as the fake commands that we’ll be using to test these interactions.

public class GenericResult
{
    public bool Succeeded { get; set; }
    public string ErrorMessage { get; set; }

    public static GenericResult Succeed()
    {
        return new GenericResult() { Succeeded = true };
    }

    public static GenericResult FailWith(string message) 
    {
        return new GenericResult() { Succeeded = false, ErrorMessage = message };
    }
}

/// By using generics we can return as many response models as needed without forcing consumers to inherit from this class for every response
public class GenericResult<T>: GenericResult 
{
    public T Model { get; set; }

    public static GenericResult<T> Succeed(T response)
    {
        return new GenericResult<T>() { Succeeded = true, Model = response };
    }

    public static GenericResult<T> FailWith(T response, string message) 
    {
        return new GenericResult() { Succeeded = false, Model = response, ErrorMessage = message };
    }
}

All of these classes and interfaces will be used in the system under test (the application we are building). Let’s see how this will look like with an actual scenario as an example.

The Scenario: Service Desk portal

Let’s say we’re building a self service portal, in .net core, that uses the Dataverse as the backend.

External users can log in to see existing incidents and submit new ones. They belong to an organization who might have purchased different support plans. To keep it simple, there are two levels of support plans: basic and premium. An organisation which is on a premium support plan can submit incidents with a higher priority than those of other organisations which are on a basic support plan. Support agents who are setup as users in the Dataverse are able to see all the incidents submitted, as well as details of the organisation and the external users who submitted them. Support plans are renewed yearly.

Entity Model Design

There are multiple ways to model this domain.

One way of doing it could probably be as an Organisation entity who can have up to one SupportPlan custom entity associated to it with a Renewal Due Date. Many users of such organisation (PortalUser) could access the portal, with their personal details (like email address) stored in the Contact entity. Finally, an organisation could have many Incidents, each of them could be raised by different PortalUsers.

   |----------------|             |---------------|
 1 |  Organisation  | 1      0..1 |  SupportPlan  |  
|--|                | ------------|               |
|  |----------------|             |---------------|
|      | 1   | 1
|      |     ---------------------------
|      | *                             | *
|  |---------------|              |---------------|
|  |    Contact    | 1       0..1 |   PortalUser  |   
|  |               | -------------|               |
|  |---------------|              |---------------| 
|                                        | 1
|  |---------------|                     |
|--|   Incident    | 1     RaisedBy      |
 * |               |---------------------|    
   |---------------|

Logic Breakdown

Once the entity model is defined, according to the requirements, portal users can submit new incidents. Based on the SupportPlan, we’ll be marking these incidents as either High or Low priority.

Next, we’ll break down functionality into things that change the state of the system (commands), and things that just retrieve the state of the system without side effects (queries).

By looking at the requirements we’ll need to know when an organisation has a basic or premium support plan, if any. It might be worth putting that piece of logic into its own dedicated query class specially if it needs to be used many times across the application. Congratulations! We have found our first query!

We also need to create a new Incident record , and based on the Organisation’s support plan, flag it as High or Low priority. It’ll change the state of the system, so that’s our first command.

Then ask yourself: What is the minimum data set we need to construct each of them? Cause that will determine the properties / params of each query / command.

For example, once a PortalUser is authenticated, we could retrieve the PortalUser data from their primary key PortalUserId, if we store it as a claim. Even better if we have both a PortalUserId and an OrganisationId claim so we wouldn’t have to traverse the relationship from the PortalUser to know which is the current organisation.

Let’s assume the application will have already available the PortalUserId and an OrganisationId for every request.

Query : GetOrganisationSupportPlanCommand

An interface to retrieve the organisation support plan could be as simple as:

public interface IGetOrganisationSupportPlanCommand: IOrgServiceCommand
{
    Guid OrganisationId { get; set; }
}

What do you think it should return? (The response model)

Consumers querying if an organisation has a support plan will probably be interested in knowing 3 things only: if there is a support plan at all, and if any, if it’s a basic or a premium one.

We might model it as nullable boolean like:

bool? // Null -> No support plan, False -> Basic Plan, True -> Premium Plan 

But then it wouldn’t be obvious without adding comments…. and comments get deprecated pretty quickly… ;)

Maybe a better solution could be a model class like:

public class SupportPlanModel 
{
    bool IsBasic { get; set; }
}

If it’s not basic, then we assume it’s premium. At least now the property has a “IsBasic” property name which gives some extra insight. But still, without a propert context of the solution we wouldn’t necessarily know (specially another developer) that !IsBasic means Premium.

Maybe we could model it as an enumeration that could match an OptionSetValue in Dataverse!

So be it!

public enum OrganisationSupportPlanType
{
    None = 0,
    Basic = 1,
    Premium = 2
}

Now it looks better!

Command : SubmitNewIncidentCommand

Now let’s look next at the command class. It’ll be used to create a new incident, with a priority field that will depend on the previous query.

Let’s do first, the exercise of deciding what is the minimum data set needed to create the incident.

This time, we would need to pass different fields from the frontend that will be stored on the Incident record, so it might be better to group them in a specific SubmitIncidentModel class.

We would need to also pass the OrganisationId and PortalUserId values from the current logged in identity.

This command will also depend on the implementation of the GetOrganisationSupportPlanCommand and we’re making that dependency explicit as a property.

public interface ISubmitNewIncidentCommand: IOrgServiceCommand
{
    Guid OrganisationId { get; set; }
    Guid PortalUserId { get; set; }
    IGetOrganisationSupportPlanCommand GetOrganisationSupportPlanCommand { get; set; }
    SubmitIncidentModel Model { get; set; }
}

public class SubmitIncidentModel 
{
    public string Title { get; set; }
    public string Description { get; set; }
}

To keep it simple, we’re just capturing the Incident title and description, also separating the fact that both OrganisationId and PortalUserId values don’t reflect values of the SubmitIncidentModel but of the current logged in Identity itself.

When mapping these commands to controller actions, they typically become very thin layers when using patterns like these. We could also even reuse the SubmitIncidentModel class in the controller action, i.e:

[Authorize]
public class IncidentsController: UserController
{
    private readonly ISubmitNewIncidentCommand _submitCommand;

    public IncidentsController(ISubmitNewIncidentCommand submitCommand)
    {
        _submitCommand = submitCommand;
    }

    [HttpPost]
    public GenericResult SubmitIncident([FromBody] SubmitIncidentModel model)
    {
        /// Identity values are retrieved from Claims
        _submitCommand.OrganisationId = Identity.OrganisationId;
        _submitCommand.PortalUserId = Identity.UserId;
        _submitCommand.Model = model;
        return _submitCommand.Execute();
    }
}

Implementation

Let’s have a look at how we could implement these.

Query: GetOrganisationSupportPlanCommand

Typically we would start with a test-driven approach. There are 3 possible support plans, so we would need to verify these 3 different outcomes with the necessary data.

When testing it might be good to use a naming convention where test classes are structured using a folder layour similar to the source classes, and using the the same names ending with “Tests”.

This will help in not only localing these tests, but particularly when looking to check if something similar was already been implemented for some area of the application.

There are 2 entities involved in checking if a Support plan exists: Organisation and Support Plans. Because the SupportPlan stores the current plan for the associated organisation as an optionset, and we don’t need any other data from the Organisation entity itself, we could just model all 3 scenarios with a single SupportPlan entity record:

  • The fact that there is no support plan for the current organisation by leaving the initial In-Memory database empty.
  • The fact that there is one basic support plan for the current organisation, by setting up one SupportPlan record whose PlanType is Basic against it.
  • The fact that there is one premium support plan for the current organisation, by setting up one SupportPlan record whose PlanType is Premium against it.

An organisation can’t have more than one support plan at the same time so we’re not interested in that scenario.

public class GetOrganisationSupportPlanCommandTests : FakeXrmEasyTestsBase
{
    private readonly IGetOrganisationSupportPlanCommand _command;
    private readonly dv_support_plan _supportPlan;
    private readonly Account _organisation;

    //The more entity records we can setup in the constructor, the smaller and easier to read each unit test will be 
    public GetOrganisationSupportPlanCommandTests()
    {
        _organisation = new Account() { Id = Guid.NewGuid() };
        _supportPlan = new dv_support_plan()
        {
            Id = Guid.NewGuid(),
            dv_organisationid = _organisation.ToEntityReference() //support plan associated to current org via this property
        };

        _command = new GetOrganisationSupportPlanCommand(_service) //fake org service from base test class
        {
            OrganisationId = _organisation.Id
        };
    }

    [Fact]
    public void Should_return_none_if_there_is_no_support_plan()
    {
        //No need to setup any test data for this scenario

        var result = _command.Execute();
        Assert.True(result.Succeeded);  //verify it succeded with no exceptions

        var supportPlan = (result as GenericResult<OrganisationSupportPlanType>).Model; //verify the response model is "None"
        Assert.Equal(OrganisationSupportPlanType.None, supportPlan);
    }

    [Theory]
    [InlineData(OrganisationSupportPlanType.Basic)]
    [InlineData(OrganisationSupportPlanType.Premium)]
    public void Should_return_relevant_organisation_plan_type_based_on_support_plans_plan_type(OrganisationSupportPlanType planType)
    {
        //We need one support plan to test this scenario:
        _supportPlan.dv_plantype = new OptionSetValue((int) planType);
        _context.Initialize(_supportPlan);

        var result = _command.Execute();
        Assert.True(result.Succeeded);

        var supportPlan = (result as GenericResult<OrganisationSupportPlanType>).Model; 
        Assert.Equal(planType, supportPlan); //matches all of InlineData combinations
    }

}

We’ve basically implemented the 3 scenarios in the test class, we’ve used a xUnit theory for support plans because the enum matches the option set value and is a great way to refactor similar tests that just change based on the input data.

Now let’s implement the associated command class.

public class GetOrganisationSupportPlanCommand : OrgServiceCommand, IGetOrganisationSupportPlanCommand
{
    public Guid OrganisationId { get; set; }

    protected override GenericResult ConcreteExecute()
    {
        using(var ctx = new XrmServiceContext(_orgService))
        {
            var supportPlan = (from sp in ctx.CreateQuery<dv_support_plan>()
                                where sp.dv_organisationid.Id == OrganisationId 
                                select sp.dv_plantype).FirstOrDefault();

            if(supportPlan == null)
            {
                return GenericResult<OrganisationSupportPlanType>.Succeed(OrganisationSupportPlanType.None);
            }
            else 
            {
                return GenericResult<OrganisationSupportPlanType>.Succeed((OrganisationSupportPlanType) supportPlan.Value);
            }
        }
    }
}

That command implementation should satisfy all unit tests above.

Moving on to the interesting part where we apply Interaction Testing now, which is the implementation of the submit new incident.

We’re going to abstract away the implementation of the GetOrganisationSupportPlanCommand implementation so we don’t depend on that data, and instead, make the GetOrganisationSupportPlanCommand return the 3 different responses.

Because we’re no longer using test that to test such interaction, we’ll be using a mocking framework, like FakeItEasy.

FakeCommandFactory

We’ll be using also a FakeCommandFactory. Because all commands return the same set of responses, either GenericResult or GenericResult<T>, it simplifies this task a lot.

The FakeCommandFactory pattern can be used to quickly create fake commands that return the response we’re expecting (we’re using them like stubs). We won’t be asserting on them, just on the main command where they’re used.

public class FakeCommandFactory
{
    public static T GetSuccessfulCommand<T>() where T: IOrgServiceCommand 
    {
        var service = A.Fake<T>();
        
        A.CallTo(() => service.Execute()).ReturnsLazily(() => {
            return GenericResult.Succeed();
        });

        return service;
    }

    public static T GetSuccessfulCommand<T, M>(M responseModel) where T: IOrgServiceCommand 
    {
        var service = A.Fake<T>();
        
        A.CallTo(() => service.Execute()).ReturnsLazily(() => {
            return GenericResult<M>.Succeed(responseModel);
        });

        return service;
    }

    public static T GetFailedCommand<T>(string errorMessage) where T: IOrgServiceCommand 
    {
        var service = A.Fake<T>();
        
        A.CallTo(() => service.Execute()).ReturnsLazily(() => {
            return GenericResult.FailWith(errorMessage);
        });

        return service;
    }

    public static T GetFailedCommand<T, M>(M responseModel, string errorMessage = "") where T: IOrgServiceCommand 
    {
        var service = A.Fake<T>();
        
        A.CallTo(() => service.Execute()).ReturnsLazily(() => {
            return GenericResult<M>.FailWith(responseModel, errorMessage);
        });

        return service;
    }
}

The FakeCommandFactory will allow us to return whatever dummy responses we need. Let’s have a look at this factory in action.

Command: SubmitNewIncidentCommand

The SubmitNewIncidentCommand will use the factory above to return the 3 possible responses: None, Basic, Premium, plus one extra: Failed due to unexpected errors.

  • If there is no support plan (None), then an error will be returned.
  • While on the Basic support plan, the Incident will be created with a Low priority.
  • While on the Premium support plan, the Incident will be created with a High priority.
  • If, for whatever reason, GetOrganisationSupportPlanCommand fails, the error is propagated accordingly

We’ll make the _supportPlanCommand below return dummy responses to verify each scenario, and then test that the Incident is created.

We would also need to test the interaction: that the GetOrganisationSupportPlanCommand is called exactly with the same OrganisationId value as the SubmitNewIncidentCommand. We defined these as properties of the interfaces, and that was for a very valid reason: FakeItEasy also automatically spies on properties, which means, we don’t have to explicitly call A.CallTo() to verify that some value in the interface was passed, just by comparing values.

public class SubmitNewIncidentCommandTests: FakeXrmEasyTestsBase
{
    private readonly ISubmitNewIncidentCommand _submitCommand;
    private readonly IGetOrganisationSupportPlanCommand _suportPlanCommand;
    
    private readonly SubmitIncidentModel _model;
    private readonly Account _organisation;
    private readonly dv_portal_user _portalUser;

    public SubmitNewIncidentCommandTests() 
    {
        _model = new SubmitIncidentModel() 
        {
            Title = "Engine is broken",
            Description = "I need a technician onsite ASAP!"
        };

        _organisation = new Account() { Id = Guid.NewGuid() };
        _portalUser = new dv_portal_user() { Id = Guid.NewGuid() };

        _submitCommand = new SubmitNewIncidentCommand(_service)
        {
            Model = _model,
            OrganisationId = _organisation.Id,
            PortalUserId = _portalUser.Id
        };
    }

    [Fact]
    public void Should_return_error_if_support_plan_failed()
    {
        //Create a command that returns a failed command
        _supportPlanCommand = FakeCommandFactory.GetFailedCommand<IGetOrganisationSupportPlanCommand>();

        //Create a new submit command and inject the support plan command as a property dependency
        _submitCommand.GetOrganisationSupportPlanCommand = _supportPlanCommand;

        //Verify submit fails in this scenario
        var result = _submitCommand.Execute();
        Assert.False(result.Succeeded);

        //Verify the interaction
        Assert.Equal(_submitCommand.OrganisationId, _supportPlanCommand.OrganisationId);
    }

    [Fact]
    public void Should_return_error_if_there_is_no_support_plan()
    {
        //Create a command that returns OrganisationSupportPlanType.None
        _supportPlanCommand = FakeCommandFactory.GetSuccessfulCommand<IGetOrganisationSupportPlanCommand, OrganisationSupportPlanType>(OrganisationSupportPlanType.None);

        //Create a new submit command and inject the support plan command as a property dependency
        _submitCommand.GetOrganisationSupportPlanCommand = _supportPlanCommand;

        //Verify submit fails in this scenario
        var result = _submitCommand.Execute();
        Assert.False(result.Succeeded);

        //Verify the interaction
        Assert.Equal(_submitCommand.OrganisationId, _supportPlanCommand.OrganisationId);
    }

    [Fact]
    public void Should_create_incident_with_low_priority_if_basic_support_plan()
    {
        //Create a command that returns OrganisationSupportPlanType.Basic
        _supportPlanCommand = FakeCommandFactory.GetSuccessfulCommand<IGetOrganisationSupportPlanCommand, OrganisationSupportPlanType>(OrganisationSupportPlanType.Basic);

        _submitCommand.GetOrganisationSupportPlanCommand = _supportPlanCommand;
        var result = _submitCommand.Execute();

        //Verify it succeeds and that a new Low priority Incident is created
        Assert.True(result.Succeeded);

        var incidentCreated = _context.CreateQuery<Incident>().FirstOrDefault();

        Assert.NotNull(incidentCreated);
        Assert.Equal(_model.Title, incidentCreated.Name);
        Assert.Equal(_model.Description, incidentCreated.Description);
        Assert.Equal(IncidentPriority.Low, (IncidentPriority) incidentCreated.dv_priority.Value);
        Assert.Equal(_organisation.Id, incidentCreated.dv_organisationid.Id);
        Assert.Equal(_portalUser.Id, incidentCreated.dv_raisedbyid.Id);

        //Verify the interaction
        Assert.Equal(_submitCommand.OrganisationId, _supportPlanCommand.OrganisationId);

    }

    [Fact]
    public void Should_create_incident_with_high_priority_if_premium_support_plan()
    {
        _supportPlanCommand = FakeCommandFactory.GetSuccessfulCommand<IGetOrganisationSupportPlanCommand, OrganisationSupportPlanType>(OrganisationSupportPlanType.Premium);

        _submitCommand.GetOrganisationSupportPlanCommand = _supportPlanCommand;
        var result = _submitCommand.Execute();

        //Verify it succeeds and that a new High priority Incident is created
        Assert.True(result.Succeeded);

        var incidentCreated = _context.CreateQuery<Incident>().FirstOrDefault();

        Assert.NotNull(incidentCreated);
        Assert.Equal(_model.Title, incidentCreated.Name);
        Assert.Equal(_model.Description, incidentCreated.Description);
        Assert.Equal(IncidentPriority.High, (IncidentPriority) incidentCreated.dv_priority.Value);
        Assert.Equal(_organisation.Id, incidentCreated.dv_organisationid.Id);
        Assert.Equal(_portalUser.Id, incidentCreated.dv_raisedbyid.Id);

        //Verify the interaction
        Assert.Equal(_submitCommand.OrganisationId, _supportPlanCommand.OrganisationId);
    }
}

And here’s how the implementation would look like:

public class SubmitNewIncidentCommand: OrgServiceCommand, ISubmitNewIncidentCommand
{
    public Guid OrganisationId { get; set; }
    public Guid PortalUserId { get; set; }
    public IGetOrganisationSupportPlanCommand GetOrganisationSupportPlanCommand { get; set; }
    public SubmitIncidentModel Model { get; set; }
    
    protected override GenericResult ConcreteExecute()
    {
        BuildCommands();

        GetOrganisationSupportPlanCommand.OrganisationId = OrganisationId;
        var supportPlanResult = GetOrganisationSupportPlanCommand.Execute();
        if(!supportPlanResult.Succeeded)
        {
            return supportPlanResult;
        }

        var supportPlan = (supportPlanResult as GenericResult<OrganisationSupportPlanType>).Model;
        if(supportPlan == OrganisationSupportPlanType.None)
        {
            return GenericResult.FailWith("The current organisation doesn't have a support plan, please buy one");
        }

        return CreateIncidentWithSupportPlan(supportPlan);
    }

    protected void BuildCommands()
    {
        if(GetOrganisationSupportPlanCommand == null)
        {
            GetOrganisationSupportPlanCommand = new GetOrganisationSupportPlanCommand(_orgService); //use the real command, not a fake one
        }
    }

    protected GenericResult CreateIncidentWithSupportPlan(OrganisationSupportPlanType planType)
    {
        _orgService.Create(new Incident()
        {
            Name = Model.Title,
            Description = Model.Description,
            dv_organisationid = new EntityReference(Account.EntityLogicalName, OrganisationId),
            dv_raisedbyid = new EntityReference(PortalUser.EntityLogicalName, PortalUserId),
            dv_priority = planType == OrganisationSupportPlanType.Premium ? 
                new OptionSetValue ((int) IncidentPriority.High) : 
                new OptionSetValue ((int) IncidentPriority.Basic)
        });

        return GenericResult.Succeed();
    }
}

Few comments about this implementation:

  • BuildCommands: it decides whether to use the property command that was injected or else use a default implementation. This is similar to injecting dependencies via properties as explained in the Dependency Injection section. The default implementation chooses one class explicitly, but you could use other patterns (i.e. ServiceLocator). We chosen to use property injection for commands because it simplifies the command initialisation in the test class as it can be moved to the constructor and just be defined there once.

  • Control Flow: because all commands return the same GenericResult and GenericResult<T> classes, it’s extremely easy to propagate responses between commands. The control logic also becomes straight-forward.

  • This structure makes it easier to break much complex logic down into simple enough pieces of logic: i.e. you could have commands of commands of commands.

Recap

  • Interaction Testing might require some extra effort in terms of both upskilling and upfront time investments but its benefits will accrue in large enterprise applications over time, specially when repeating test data becomes harder than embracing these patterns.
  • Think about if some logic can be reused in many different places: that could be a candidate for interaction testing.
  • Developing and testing applications with FakeXrmEasy alone focuses on the “What” should the system do, as opposed to “How” it should be done. The coupling is with the data model, not much about the implementation.
  • The more interactions you have the more you are testing the “How” the system should behave vs. the “What” it should do, and you introduce more coupling between the system under test and the tests themselves.
  • However interaction testing allows abstracting away the data model from their consumers: for example, if, for whatever reason the SupportPlan optionset had to be moved elsewhere, we would have to change the GetOrganisationSupportPlanCommand only, without touching any consumers of that command. We wouldn’t have to repeat the SupportPlan entity record across all of the consumers of that command either, making the test data of these commands smaller.
  • Command query separation is still a valid design pattern regardless if you apply interaction testing principles or not, because it allows for easily testing, modifications, and refactoring of any piece of logic separately. You might even do it to reduce cognitive/cyclomatic complexity alone.

Interaction Testing should be a must have skill of both Solution Architects and/or Lead Developer roles and it should be embraced by the team when there is a need for it.