C# 为对象列表创建 Mock Configuration.GetSection(“Section:SubSection”)
C# Create Mock Configuration.GetSection(“Section:SubSection”) for objects list
Objective
使用 Moq 和 XUnit 创建一个模拟对象,用于加载特定部分“Character/Skills”以增强单元测试的覆盖率。
SUT(在某些时候),加载设置的方式
var skills = Configuration.GetSection(“Character:Skills”);
来自以下应用程序设置:
{
"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..."
}
之前的阅读
阅读这些帖子(以及其他其他帖子,作为谷歌搜索的结果),我注意到我们只能使用原始的“字符串”数据类型或者 new Mock 对象(没有设置):
- Stack Overflow - how to mock Configuration.GetSection(“foo:bar”),
- Mocking IConfiguration extension method
- Mocking IConfiguration Getvalue() extension method in Unit Test
约束: 将 appSetting 文件复制到 TestProject(或创建 MemoryStream)以加载真实设置可以解决这种情况,但测试将改为“集成” “单位”;因为存在 I/O 依赖关系。
方法
代码的想法(稍后显示)是模拟每个 属性(key/id),然后将它们合并到类似于此的树中:
- "字符" ------ 要读取的配置,使用
GetSection()
然后 Get<T>()
- "技能" ------ 合并属性的配置列表
- "Key" - "CQC Combat" ------ 原始值1
- "Id" - "15465" ------ 原始值 2
代码
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);
预期结果
运行原来的系统,我得到的是实例化的list及其各自的
这是屏幕截图:
模拟结果
上面的代码,我只得到了实例化的列表,但所有属性return null。
这是屏幕截图:
最后我重构了代码,去掉了整个 foreach
块,并用下面的代码替换了列表初始化 var mockConfSections = new List<IConfigurationSection>();
,这样更简单、更清晰。
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();
说明
由于之前的实现构建了一个带有模拟节点的配置树,因此需要为每个节点构建一个设置和 return,从而导致一个臃肿的解决方案。
根据文章 Keeping Configuration Settings in Memory, I projected the list with flattened Key/Id Dictionary using the LINQ SelectMany,然后构建内存配置,最后用“真实节点”模拟设置,得到一个模拟设置。
Objective
使用 Moq 和 XUnit 创建一个模拟对象,用于加载特定部分“Character/Skills”以增强单元测试的覆盖率。
SUT(在某些时候),加载设置的方式
var skills = Configuration.GetSection(“Character:Skills”);
来自以下应用程序设置:
{
"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..."
}
之前的阅读
阅读这些帖子(以及其他其他帖子,作为谷歌搜索的结果),我注意到我们只能使用原始的“字符串”数据类型或者 new Mock
- Stack Overflow - how to mock Configuration.GetSection(“foo:bar”),
- Mocking IConfiguration extension method
- Mocking IConfiguration Getvalue() extension method in Unit Test
约束: 将 appSetting 文件复制到 TestProject(或创建 MemoryStream)以加载真实设置可以解决这种情况,但测试将改为“集成” “单位”;因为存在 I/O 依赖关系。
方法
代码的想法(稍后显示)是模拟每个 属性(key/id),然后将它们合并到类似于此的树中:
- "字符" ------ 要读取的配置,使用
GetSection()
然后Get<T>()
- "技能" ------ 合并属性的配置列表
- "Key" - "CQC Combat" ------ 原始值1
- "Id" - "15465" ------ 原始值 2
- "技能" ------ 合并属性的配置列表
代码
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);
预期结果
运行原来的系统,我得到的是实例化的list及其各自的 这是屏幕截图:
模拟结果
上面的代码,我只得到了实例化的列表,但所有属性return null。 这是屏幕截图:
最后我重构了代码,去掉了整个 foreach
块,并用下面的代码替换了列表初始化 var mockConfSections = new List<IConfigurationSection>();
,这样更简单、更清晰。
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();
说明
由于之前的实现构建了一个带有模拟节点的配置树,因此需要为每个节点构建一个设置和 return,从而导致一个臃肿的解决方案。
根据文章 Keeping Configuration Settings in Memory, I projected the list with flattened Key/Id Dictionary using the LINQ SelectMany,然后构建内存配置,最后用“真实节点”模拟设置,得到一个模拟设置。