C # Create Mock Configuration.GetSection (“Section: SubSection”) para a lista de objetos
Objetivo
Crie um objeto mock, usando Moq e XUnit, para carregar a seção específica "Personagem / Habilidades" para melhorar a cobertura em testes de unidade.
O SUT (em algum ponto), carrega a configuração no caminho
var skills = Configuration.GetSection(“Character:Skills”);
A partir do seguinte 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..."
}
Leitura Anterior
Lendo esses posts (e outros, como resultado de Googling), percebi que só podemos usar um tipo de dados primitivo "string" ou então um novo objeto Mock <IConfigurationSection> (sem configuração):
- Stack Overflow - como simular Configuration.GetSection (“foo: bar”) ,
- Mocking IConfiguration extension method
- Mocking IConfiguration Getvalue () método de extensão em Unit Test
Restrição: Copiar o arquivo appSetting no TestProject (ou criar um MemoryStream) para carregar as configurações reais poderia resolver este cenário, mas o teste seria uma "Integração" em vez de "Unidade"; uma vez que existe uma dependência de E / S.
A abordagem
A ideia do código (mostrada mais tarde) é simular cada propriedade (chave / id) e, em seguida, mesclá-los em uma árvore semelhante a esta:
- "Character" ------ Configuração a ser lida, usando
GetSection()e entãoGet<T>()- "Habilidades" ------ Lista de configuração com atributo mesclado
- "Chave" - "Combate CQC" ------ Valor primitivo 1
- "Id" - "15465" ------ Valor primitivo 2
- "Habilidades" ------ Lista de configuração com atributo mesclado
O código
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);
Resultado esperado
Executando o sistema original, obtenho a lista instanciada com seus respectivos. Aqui está a imagem:
Resultado zombado
O código acima, recebo apenas a lista instanciada, mas todos os atributos retornam nulos. Aqui está a imagem:
Respostas
Por fim, refatorei o código, eliminando todo o foreachbloco e substituindo a inicialização da lista var mockConfSections = new List<IConfigurationSection>();pelo seguinte trecho de código, que é mais simples e limpo.
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();
Explicação
Como a implementação anterior construiu uma árvore de configuração com nós simulados, houve a necessidade de construir uma configuração e retorno para cada um, resultando em uma solução inchada.
Com base no artigo Mantendo as definições de configuração na memória , projetei a lista com dicionário de chave / id achatado usando o LINQ SelectMany , construí a configuração de memória e finalmente simulei a configuração com "nós reais", resultando em uma configuração simulada.