4

Objective

Create a mock object, using Moq and XUnit, for loading the specific section "Character/Skills" to enhance the coverage in unit testing.

The SUT (in some point), loads the setting in the way

var skills = Configuration.GetSection(“Character:Skills”);

From the following appSetting:

{
    "dummyConfig1": {
        "Description": "bla bla bla...",
    },
    "Character": {
    "Name": "John Wick",
    "Description": "A retired hitman seeking vengeance for the killing of the dog given to him...",
    "Skills": [
        {
        "Key": "CQC Combat",
        "Id": "15465"
        },
        {
        "Key": "Firearms",
        "Id": "14321"
        },
        {
        "Key": "Stealth",
        "Id": "09674"
        },
        {
        "Key": "Speed",
        "Id": "10203"
        }
    ],
    "DummyConf2": "more bla bla bla..."
}

Previous Reading

Reading these posts (and other others, as result of Googling), I noticed that we can only use a primitive "string" datatype or else new Mock<IConfigurationSection> object (with no setting):

Constraint: Copying the appSetting file into the TestProject (or create a MemoryStream) to load the real settings could solve this scenario, but the test would be a "Integration" instead of "Unit"; since there is an I/O dependency.

The approach

The code's idea (shown later) is mocking each property (key/id) and then merging them in a tree similar to this:

  • "Character" ------ Configuration to be read, using GetSection() and then Get<T>()
    • "Skills" ------ Configuration list with merged attribute
      • "Key" - "CQC Combat" ------ Primitive value 1
      • "Id" - "15465" ------ Primitive value 2

The Code

var skillsConfiguration = new List<SkillsConfig>
{
    new SkillsConfig { Key = "CQC Combat"   , Id = "15465" },
    new SkillsConfig { Key = "Firearms"     , Id = "14321" },
    new SkillsConfig { Key = "Stealh"       , Id = "09674" },
    new SkillsConfig { Key = "Speed"        , Id = "10203" },
};

var configurationMock = new Mock<IConfiguration>();
var mockConfSections = new List<IConfigurationSection>();

foreach (var skill in skillsConfiguration)
{
    var index = skillsConfiguration.IndexOf(skill);

    //Set the Key string value
    var mockConfSectionKey = new Mock<IConfigurationSection>();
    mockConfSectionKey.Setup(s => s.Path).Returns($"Character:Skills:{index}:Key");
    mockConfSectionKey.Setup(s => s.Key).Returns("Key");
    mockConfSectionKey.Setup(s => s.Value).Returns(skill.Key);

    //Set the Id string value
    var mockConfSectionId = new Mock<IConfigurationSection>();
    mockConfSectionId.Setup(s => s.Path).Returns($"Character:Skills:{index}:Id");
    mockConfSectionId.Setup(s => s.Key).Returns("Id");
    mockConfSectionId.Setup(s => s.Value).Returns(skill.Id);

    //Merge the attribute "key/id" as Configuration section list
    var mockConfSection = new Mock<IConfigurationSection>();                
    mockConfSection.Setup(s => s.Path).Returns($"Character:Skills:{index}");
    mockConfSection.Setup(s => s.Key).Returns(index.ToString());
    mockConfSection.Setup(s => s.GetChildren()).Returns(new List<IConfigurationSection> { mockConfSectionKey.Object, mockConfSectionId.Object });    
    
    //Add the skill object with merged attributes
    mockConfSections.Add(mockConfSection.Object);
}

// Add the Skill's list
var skillsMockSections = new Mock<IConfigurationSection>();
skillsMockSections.Setup(cfg => cfg.Path).Returns("Character:Skills");
skillsMockSections.Setup(cfg => cfg.Key).Returns("Skills");
skillsMockSections.Setup(cfg => cfg.GetChildren()).Returns(mockConfSections);

//Mock the whole section, for using GetSection() method withing SUT
configurationMock.Setup(cfg => cfg.GetSection("Character:Skills")).Returns(skillsMockSections.Object);

Expected result

Running the original system, I get the instantiated list with its respective Here is the screenshot:

appsetting loaded sucessfully

Mocked result

The code above, I only get the instantiated list but all attributes return null. Here is the screenshot:

appsetting mocked with null values

5
  • Why just don't use MemoryConfigurationSource and setup a plain key value configuration? Commented Sep 1, 2020 at 19:01
  • Because it implements MemoryStream and hence, would be a "Integration Test" instead of "Unit"; since there is an I/O dependency. I would like to work with mocked vaules. Commented Sep 1, 2020 at 19:46
  • It implements IConfigurationSource interface and inherits Object, there is nothing related to IO or memory, except the name Commented Sep 1, 2020 at 20:02
  • @PavelAnikhouski I jusr answer myself, based on your insight. Thanks. Commented Sep 3, 2020 at 0:58
  • Make your class to depend on IOptions<ConfigurationObject> instead of IConfiguration, then you don't need to mock it. ;) Commented Sep 3, 2020 at 7:35

1 Answer 1

3

Finally I refactored the code, getting rid the whole foreach block and replacing the list initialization var mockConfSections = new List<IConfigurationSection>(); with the follow piece of code, which is simpler and cleaner.

var fakeSkillSettings = skillsConfiguration.SelectMany(
    skill => new Dictionary<string, string> {
        { $"Character:Skills:{skillsConfiguration.IndexOf(skill)}:Key", skill.Key },
        { $"Character:Skills:{skillsConfiguration.IndexOf(skill)}:Id" , skill.Id  },
});

var configBuilder = new ConfigurationBuilder();
var mockConfSections = configBuilder.AddInMemoryCollection(fakeSkillSettings)
    .Build()
    .GetSection("Character:Skills")
    .GetChildren();

Explanation

As the previous implementation built a configuration tree with mocked nodes, there was a need to build a setup and return for each one, resulting in a bloated solution.

Based on the article Keeping Configuration Settings in Memory, I projected the list with flattened Key/Id Dictionary using the LINQ SelectMany, then built the memory configuration and finally mocked the setting with "real nodes", resulting in one mock setup.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.