简体   繁体   中英

C# Create Mock Configuration.GetSection(“Section:SubSection”) for objects list

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加载成功

Mocked result

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

appsetting 用空值模拟

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM