Skip to main content

Unit test of Manual Rules and Bags

We highly recommend that you create unit tests for your manual Rules and Bags.

Unit testing is a widely used industry practice that both helps you achieve improved quality but also is an efficient way to initially develop and test your implementation without having to run an actual migration of a Business Object in order to test your code.

You can use any test framework and any mocking library to create unit tests. In the samples below, we have used

XUnit
for unit test
FakeItEasy
for mocking
Shouldly
for easy-to-read test assertion

In order to be able to unit test Bags and Rules, you need to mock the appropriate context interface. There are 3 different interfaces to consider:

EngineInterface
Source Engine
  • IItemExporterContext
Target Engine
  • IItemImportContext
  • IItemImportContextMapping (contains a couple of methods only available to Mapping Rules)

These interfaces contain the properties and methods available to Bags and Rules. In order to unit test a given Bag/Rule, you only need to mock the members on the interface that is actually used.

As an example, let's look at the Target Engine Mapping Rule GetFrequencyId from the Hopp Academy Hopp Software Training course.

GetFrequencyId is a manual rule, and the implementation looks like this:

using System;
using System.Linq;

namespace MigFx.Engine.Project.Target
{
partial class MappingRules
{
public override Decimal? GetFrequencyId(FlagHandler flag, string frequencyShortName)
{
// FrequencyShortName is missing
if (string.IsNullOrWhiteSpace(frequencyShortName))
{
flag(2);
return null;
}

var row = Valueset.Frequencies.Where(r => r.FrequencyShortName == frequencyShortName)
.Select(r => new { r.FrequencyId })
.FirstOrDefault();

if (row != null)
{
return row.FrequencyId;
}
else
{
flag(1);
return null;
}
}
}
}

The rule takes a FlagHandler delegate to use to raise flags and a frequencyShortName argument

  • If the frequencyShortName is null or empty, the rule uses the flagHandler delegate to raise flag 2 and then returns null
  • If not, the rule looks up the frequencyShortName in the Valuset Frequencies
  • If a row is not found, the rule raises flag 1 and returns null
  • If a row is found, the rule raises no flags and returns the FrequencyId from the row.

In the following, we will look at 3 unit tests for the GetFrequencyId mapping rule:

  • ShouldRaiseFlag2IfFrequencyShortNameNotProvided
  • ShouldRaiseFlag1IfFrequencyNotFound
  • ShouldReturnFoundValueIfSuccessful

A couple of things to note about all manual rules and bags

  • In the Target Engine, Manual Rules and Bags exist in the namespace MigFx.Engine.Project.Target
  • In the Source Engine, Manual Rules and Bags exist in the namespace MigFx.Source.Project
  • Manual Rules are in fact virtual methods in an abstract, generated base class that are overridden in a generated, derived class marked as partial
  • Each rule is implemented in a separate file that is a partial implementation of this derived class
    • For example, the GetFrequencyId rule above is a method inside the partial class MappingRules

In order to test the GetFrequencyId rule, you need to instantiate an instance of the MappingRules class and call the GetFrequencyId method.

Since this is a Target Engine Mapping Rule, the constructor for the MappingRules class takes a single parameter of type IItemImportContextMapping.

ShouldRaiseFlag2IfFrequencyShortNameNotProvided

This is the simplest test, since this execution path in the rule does not access any members on the IItemImportContextMapping so a minimal of mocking is required.

[Fact]
public void ShouldRaiseFlag2IfFrequencyShortNameNotProvided()
{
// Arrange
var ctx = A.Fake<IItemImportContextMapping>();
var sut = new MappingRules(ctx);
var flags = new List<int>();

// A local function to act as FlagHandler
bool flagHandler(int flag)
{
flags.Add(flag);
return true;
};

// Act
var result = sut.GetFrequencyId(flagHandler, null);

// Assert
result.Should().BeNull("Rule should return null");
flags.Count.Should().Be(1, "Should raise exactly 1 flag");
flags.Should().Contain(f => f == 2, "Should raise flag 2");
}

// Arrange

  • Using FakeItEasy, create a mock context (a 'fake') of the IItemImportContextMapping
  • Get a sut (System under Test) by constructing an instance of the MappingRules class, passing in the fake context to the constructor.
  • Create a flags list of int to keep track of the flags raised by the rule
  • Create a local method with the same signature as the FlagHandler delegate to be passed to the rule. This method stores all flags raised by the rule in the flags list

// Act

  • Test the rule by calling the rule method on the sut with the local flagHandler and a null value for the frequencyShortName
  • Store the return value in the result variable

// Assert

  • Using Shouldly, assert that
    • The rule returns a null value
    • Exactly one flag is raised
    • The flag number 2 is raised

ShouldRaiseFlag1IfFrequencyNotFound

For this test, the rule will attempt to lookup a row in the Frequencies Valueset. So, this Valueset must be mocked.

Since the test is that rule should not find anything, it is sufficient to mock an empty Valueset.

[Fact]
public void ShouldRaiseFlag1IfFrequencyNotFound()
{
// Arrange
var valuesets = A.Fake<IValuesets>();

A.CallTo(() => valuesets.Frequencies)
.Returns(Enumerable.Empty<Valueset.IFrequencies>()
.AsQueryable());

var ctx = A.Fake<IItemImportContextMapping>();

A.CallTo(() => ctx.Valueset).Returns(valuesets);

var sut = new MappingRules(ctx);

var flags = new List<int>();

// A local function to act as FlagHandler
bool flagHandler(int flag)
{
flags.Add(flag);
return true;
};

// Act
var result = sut.GetFrequencyId(flagHandler, "should not be found");

// Assert
result.Should().BeNull("Rule should return null");
flags.Count.Should().Be(1, "Should raise exactly 1 flag");
flags.Should().Contain(f => f == 1, "Should raise flag 1");
}

// Arrange

  • Using FakeItEasy
    • Create a fake IValuesets instance. The IValuesets interface is generated and contains a get property for each Valueset
    • Mock the Frequencies property on the fake IValuesets to return an empty collection of valueset rows. The Valueset.IFrequencies interface is created by the engine generator and contains get properties for the columns of the valueset
    • Create a fake IItemImportContextMapping instance
    • Mock a call to the Valueset property on the fake context to return the fake IValuesets created earlier
  • Get a sut (System under Test) by constructing an instance of the MappingRules class, passing the fake context.
  • Create a flags list of int to keep track of the flags raised by the rule
  • Create a local method with the same signature as the FlagHandler delegate to be passed to the rule. This method stores all flags raised by the rule in the flags list

// Act

  • Test the rule by calling the rule method on the sut with the local flagHandler and a dummy value for the frequencyShortName
  • Store the return value in the result variable

// Assert

  • Using Shouldly, assert that
    • The rule returns a null value
    • Exactly one flag is raised
    • The flag number 1 is raised

ShouldReturnFoundValueIfSuccessful

For this test, the rule will again attempt to lookup a row in the Frequencies Valueset and this Valueset must be mocked.

Since the test is that the rule should now successfully find a row, the mocked Valueset must contain a row to find - plus a couple of other rows to ensure it actually finds the correct one.

[Fact]
public void ShouldReturnFoundValueIfSuccessful()
{
// Arrange
var frequencyName = "Test";
var frequencyId = 1000;
var frequencies = new List<Valueset.FrequenciesRow>
{
new() { FrequencyId = frequencyId - 1, FrequencyShortName = frequencyName + "_A" },
new() { FrequencyId = frequencyId, FrequencyShortName = frequencyName }, // Row to find
new() { FrequencyId = frequencyId + 1, FrequencyShortName = frequencyName + "_B" }
};

var valuesets = A.Fake<IValuesets>();

A.CallTo(() => valuesets.Frequencies)
.Returns(frequencies.AsQueryable());

var ctx = A.Fake<IItemImportContextMapping>();

A.CallTo(() => ctx.Valueset)
.Returns(valuesets);

var sut = new MappingRules(ctx);

var flags = new List<int>();

// A local function to act as FlagHandler
bool flagHandler(int flag)
{
flags.Add(flag);
return true;
};

// Act
var result = sut.GetFrequencyId(flagHandler, frequencyName);

// Assert
flags.Count.Should().Be(0, "Should raise no flags");
result.Should().Be(frequencyId, "Should return the frequencyId found in the Valueset");
}

// Arrange

  • Set up a couple of variables to contain the frequencyShortName to lookup and the expected frequencyId to be found
  • Using FakeItEasy
    • Create a fake IValuesets instance. The IValuesets interface is created by the engine generator and contains a get property for each Valueset

    • Create collection of valueset rows. One of the rows has the correct value for frequencyShortName and frequencyId.

      The Valueset.FrequenciesRow class is generated and contains getter and setter properties for the Valueset columns

    • Mock a call to the Frequencies property on the fake IValuesets to return collection of valueset rows created above

    • Create a fake IItemImportContextMapping instance

    • Mock a call to the Valueset property on the fake context to return the fake IValuesets created earlier

  • Get a sut (System under Test) by constructing an instance of the MappingRules class, passing the fake context.
  • Create a flags list of int to keep track of the flags raised by the rule
  • Create a local method with the same signature as the FlagHandler delegate to be passed to the rule. This method stores all flags raised by the rule in the flags list

// Act

  • Test the rule by calling the rule method on the sut with the local flagHandler and the correct frequencyShortName stored above
  • Store the return value in the result variable

// Assert

  • Using Shouldly, assert that
    • The rule does not raise any flags
    • The rule returns the expected frequencyId from the correct valueset row