Unit test of Manual Rules and Bags

Modified on Fri, 13 Oct 2023 at 07:07 PM

This is a detailed topic in our support portal in the Using Hopp series and assumes that you have some prior knowledge or experience using Hopp. 


From version 1.11. Hopp makes the generated engine code unit test friendly. It is now pretty straight forward to write unit tests. If you are implementing Manual Rules and Bags in Visual Studio, this article is for you.


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


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:


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 Online training exercises: 


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;
      }
    }
  }
}
C#

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

  • If the frequencyShortName is null or empty, the rule raises flag 2 and return null
  • If not, the rule looks up the frequencyShortName in the Valuset Frequencies
  • If a row is not found, it raises flag 1 and returns null
  • If a row is found it raises no flags and returns the FrequencyId on 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 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>();

  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");
}
C#

// Arrange

  • Using FakeItEasy, create a mock (a 'fake') of the IItemImportContextMapping
  • Get a sut (System under Test) by constructing an instance of the MappingRules class, passing the fake context.
  • Create a 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. All flags raised by the rule will be stored in the list of int

// 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 Fluent Assertions, 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>();

  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");
}
C#

// Arrange

  • Using FakeItEasy
    • Create a fake IValuesets instance. The IValuesets interface is generated and contains a get property for each Valueset 
    • Mock a call to the Frequencies property on the fake IValuesets to return an empty collection of valueset rows. The Valueset.IFrequencies interface is generated 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 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. All flags raised by the rule will be stored in the list of int

// 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 Fluent Assertions, 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 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>();

  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");
}
C#

// 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 generated 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 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. All flags raised by the rule will be stored in the list of int

// 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 Fluent Assertions, assert that
    • The rule does not raise any flags
    • The rule returns the expected frequencyId from the correct valueset row 


Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select atleast one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article