Заполните IConfiguration для модульных тестов

Конфигурация .NET Core позволяет так много параметров для добавления значений (переменные среды, файлы json, аргументы командной строки).

Я просто не могу понять и найти ответ, как заполнить его с помощью кода.

Я пишу модульные тесты для методов расширения конфигураций, и я подумал, что заполнить их в модульных тестах с помощью кода будет проще, чем загружать выделенные файлы json для каждого теста.

Мой текущий код:

[Fact]
public void Test_IsConfigured_Positive()
{

  // test against this configuration
  IConfiguration config = new ConfigurationBuilder()
    // how to populate it via code
    .Build();

  // the extension method to test
  Assert.True(config.IsConfigured());

}

Обновлять:

Особый случай - это пустой раздел, который в json будет выглядеть так.

{
  "MySection": {
     // the existence of the section activates something triggering IsConfigured to be true but does not overwrite any default value
   }
 }

Обновление 2:

Как отметил Мэтью в комментариях, наличие пустого раздела в json дает тот же результат, что и отсутствие раздела вообще. Я привел пример, и да, это так. Я ошибался, ожидая другого поведения.

Итак, что мне делать и чего я ожидал:

Я пишу модульные тесты для двух методов расширения для IConfiguration (фактически потому, что привязка значений в методе Get ... Settings не работает по какой-то причине (но это другая тема). Они выглядят так:

public static bool IsService1Configured(this IConfiguration configuration)
{
  return configuration.GetSection("Service1").Exists();
}

public static MyService1Settings GetService1Settings(this IConfiguration configuration)
{
  if (!configuration.IsService1Configured()) return null;

  MyService1Settings settings = new MyService1Settings();
  configuration.Bind("Service1", settings);

  return settings;
}

Мое неправильное понимание заключалось в том, что если я помещу пустой раздел в настройках приложения, метод IsService1Configured() вернет true (что, очевидно, сейчас неверно). Разница, которую я ожидал, заключается в том, что теперь у пустого раздела метод GetService1Settings() возвращает null, а не так, как я ожидал MyService1Settings со всеми значениями по умолчанию.

К счастью, это все еще работает для меня, так как у меня не будет пустых разделов (или теперь я знаю, что мне нужно избегать таких случаев). Это был всего лишь один теоретический случай, с которым я столкнулся при написании модульных тестов.

Далее по дороге (для желающих).

Для чего я его использую? Активация / деактивация услуги на основе конфигурации.

У меня есть приложение, в которое встроена служба / некоторые службы. В зависимости от развертывания мне необходимо полностью активировать / деактивировать службы. Это связано с тем, что некоторые (локальные или тестовые настройки) не имеют полного доступа к полной инфраструктуре (вспомогательные службы, такие как кеширование, метрики ...). И я делаю это через настройки приложения. Если служба настроена (раздел конфигурации существует), он будет добавлен. Если раздел конфигурации отсутствует, он не будет использоваться.


Полный код для дистиллированного примера приведен ниже.

  • в Visual Studio создайте новый API с именем WebApplication1 из шаблонов (без HTTPS и аутентификации)
  • удалите класс Startup и appsettings.Development.json
  • замените код в Program.cs приведенным ниже кодом.
  • теперь в appsettings.json вы можете активировать / деактивировать сервисы, добавив / удалив секции Service1 и Service2
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;

namespace WebApplication1
{

  public class MyService1Settings
  {
  public int? Value1 { get; set; }
  public int Value2 { get; set; }
  public int Value3 { get; set; } = -1;
  }

  public static class Service1Extensions
  {

  public static bool IsService1Configured(this IConfiguration configuration)
  {
  return configuration.GetSection("Service1").Exists();
  }

  public static MyService1Settings GetService1Settings(this IConfiguration configuration)
  {
  if (!configuration.IsService1Configured()) return null;

  MyService1Settings settings = new MyService1Settings();
  configuration.Bind("Service1", settings);

  return settings;
  }

  public static IServiceCollection AddService1(this IServiceCollection services, IConfiguration configuration, ILogger logger)
  {

  MyService1Settings settings = configuration.GetService1Settings();

  if (settings == null) throw new Exception("loaded MyService1Settings are null (did you forget to check IsConfigured in Startup.ConfigureServices?) ");

  logger.LogAsJson(settings, "MyServiceSettings1: ");

  // do what ever needs to be done

  return services;
  }

  public static IApplicationBuilder UseService1(this IApplicationBuilder app, IConfiguration configuration, ILogger logger)
  {

  // do what ever needs to be done

  return app;
  }

  }

  public class Program
  {

    public static void Main(string[] args)
    {
      CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
      WebHost.CreateDefaultBuilder(args)
      .ConfigureLogging
        (
        builder => 
          {
            builder.AddDebug();
            builder.AddConsole();
          }
        )
      .UseStartup<Startup>();
      }

    public class Startup
    {

      public IConfiguration Configuration { get; }
      public ILogger<Startup> Logger { get; }

      public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
      {
      Configuration = configuration;
      Logger = loggerFactory.CreateLogger<Startup>();
      }

      // This method gets called by the runtime. Use this method to add services to the container.
      public void ConfigureServices(IServiceCollection services)
      {

      // flavour 1: needs check(s) in Startup method(s) or will raise an exception
      if (Configuration.IsService1Configured()) {
      Logger.LogInformation("service 1 is activated and added");
      services.AddService1(Configuration, Logger);
      } else 
      Logger.LogInformation("service 1 is deactivated and not added");

      // flavour 2: checks are done in the extension methods and no Startup cluttering
      services.AddOptionalService2(Configuration, Logger);

      services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {

      if (env.IsDevelopment()) app.UseDeveloperExceptionPage();

      // flavour 1: needs check(s) in Startup method(s) or will raise an exception
      if (Configuration.IsService1Configured()) {
        Logger.LogInformation("service 1 is activated and used");
        app.UseService1(Configuration, Logger); }
      else
        Logger.LogInformation("service 1 is deactivated and not used");

      // flavour 2: checks are done in the extension methods and no Startup cluttering
      app.UseOptionalService2(Configuration, Logger);

      app.UseMvc();
    }
  }

  public class MyService2Settings
  {
    public int? Value1 { get; set; }
    public int Value2 { get; set; }
    public int Value3 { get; set; } = -1;
  }

  public static class Service2Extensions
  {

  public static bool IsService2Configured(this IConfiguration configuration)
  {
    return configuration.GetSection("Service2").Exists();
  }

  public static MyService2Settings GetService2Settings(this IConfiguration configuration)
  {
    if (!configuration.IsService2Configured()) return null;

    MyService2Settings settings = new MyService2Settings();
    configuration.Bind("Service2", settings);

    return settings;
  }

  public static IServiceCollection AddOptionalService2(this IServiceCollection services, IConfiguration configuration, ILogger logger)
  {

    if (!configuration.IsService2Configured())
    {
      logger.LogInformation("service 2 is deactivated and not added");
      return services;
    }

    logger.LogInformation("service 2 is activated and added");

    MyService2Settings settings = configuration.GetService2Settings();
    if (settings == null) throw new Exception("some settings loading bug occured");

    logger.LogAsJson(settings, "MyService2Settings: ");
    // do what ever needs to be done
    return services;
  }

  public static IApplicationBuilder UseOptionalService2(this IApplicationBuilder app, IConfiguration configuration, ILogger logger)
  {

    if (!configuration.IsService2Configured())
    {
      logger.LogInformation("service 2 is deactivated and not used");
      return app;
    }

    logger.LogInformation("service 2 is activated and used");
    // do what ever needs to be done
    return app;
  }
}

  public static class LoggerExtensions
  {
    public static void LogAsJson(this ILogger logger, object obj, string prefix = null)
    {
      logger.LogInformation(prefix ?? string.Empty) + ((obj == null) ? "null" : JsonConvert.SerializeObject(obj, Formatting.Indented)));
    }
  }

}

person monty    schedule 03.04.2019    source источник


Ответы (5)


Вы можете использовать MemoryConfigurationBuilderExtensions, чтобы предоставить его через словарь.

using Microsoft.Extensions.Configuration;

var myConfiguration = new Dictionary<string, string>
{
    {"Key1", "Value1"},
    {"Nested:Key1", "NestedValue1"},
    {"Nested:Key2", "NestedValue2"}
};

var configuration = new ConfigurationBuilder()
    .AddInMemoryCollection(myConfiguration)
    .Build();

Эквивалентный JSON будет:

{
  "Key1": "Value1",
  "Nested": {
    "Key1": "NestedValue1",
    "Key2": "NestedValue2"
  }
}

Эквивалентные переменные среды будут (при условии отсутствия нечувствительности к префиксу / регистру):

Key1=Value1
Nested__Key1=NestedValue1
Nested__Key2=NestedValue2
person Matthew    schedule 03.04.2019
comment
Да, это сработает. Я обновил свой вопрос, чтобы отразить недостающий фрагмент. - person monty; 03.04.2019
comment
Вам следует обновить свой вопрос, включив в него то, что вы ожидаете от него. Наличие пустого узла JSON приводит к тому же результату, что и отсутствие этого узла вообще. - person Matthew; 03.04.2019
comment
вы действительно были правы. Пустой раздел вроде бы удален и не существует. Я добавил Обновление 2 к своему вопросу с полным примером того, что я (ошибочно) ожидал, и почему. - person monty; 04.04.2019
comment
ну, модульный тест показал, что привязка конфигурации не удалась, потому что я только что определил получение для свойства, а не набор. - person monty; 04.04.2019
comment
Мне не нравится заполнитель для этого ввода. В духе ТАК, пытающегося улучшить сообщество, я фактически добавлю +1 к этому ответу! Это было действительно полезно. Я почти реализовал свою IConfiguration для тестирования. ???????? - person xanadont; 08.10.2019
comment
@xanadont Я не понимаю вашего утверждения, вы говорите, что имена были плохо написаны для примера? Я не против обновить ответ, чтобы сделать что-то более разумное, я просто хочу убедиться, что вы имеете в виду именно это. - person Matthew; 16.10.2020
comment
Понятия не имею, что я имел в виду год назад. ????????‍♂️ - person xanadont; 16.10.2020
comment
@Matthew, добавив json в ваш пример, будет полезно - person Philippe; 19.10.2020

Решение, к которому я пришел (которое, по крайней мере, отвечает на заголовок вопроса!), - это использовать файл настроек в решении testsettings.json и установить для него значение «Всегда копировать».

private IConfiguration _config;

public UnitTestManager()
{
    IServiceCollection services = new ServiceCollection();

    services.AddSingleton<IConfiguration>(Configuration);
}

public IConfiguration Configuration
{
    get
    {
        if (_config == null)
        {
            var builder = new ConfigurationBuilder().AddJsonFile($"testsettings.json", optional: false);
            _config = builder.Build();
        }

        return _config;
    }
}
person noelicus    schedule 13.02.2020
comment
Привет, ребята, кажется, что AddJsonFile немного изменен на стороне источника .net 5.0: https: //docs.microsoft.com/tr-tr/dotnet/api/microsoft.extensions.configuration.jsonconfigurationextensions.addjsonfile? view = dotnet-plat-ext-5.0 # Microsoft_Extensions_Configuration_JsonConfigurationExtensions_AddJsonFile_Microonsoft - person Beyto; 23.03.2021

Будет ли AddInMemoryCollection справка по методу расширения?

Вы можете передать в него коллекцию "ключ-значение": IEnumerable<KeyValuePair<String,String>> с данными, которые могут вам понадобиться для теста.

var builder = new ConfigurationBuilder();

builder.AddInMemoryCollection(new Dictionary<string, string>
{
     { "key", "value" }
});
person Anton Sizikov    schedule 03.04.2019
comment
Да, это сработает. Я обновил свой вопрос, чтобы отразить недостающий фрагмент. - person monty; 03.04.2019

Я предпочитаю, чтобы классы моих приложений не зависели от IConfiguration. Вместо этого я создаю класс конфигурации для хранения конфигурации с конструктором, который может инициализировать его из IConfiguration следующим образом:

public class WidgetProcessorConfig
{
    public int QueueLength { get; set; }
    public WidgetProcessorConfig(IConfiguration configuration)
    {
        configuration.Bind("WidgetProcessor", this);
    }
    public WidgetProcessorConfig() { }
}

то в вашем ConfigureServices вам просто нужно сделать:

services.AddSingleton<WidgetProcessorConfig>();
services.AddSingleton<WidgetProcessor>();

и для тестирования:

var config = new WidgetProcessorConfig
{
    QueueLength = 18
};
var widgetProcessor = new WidgetProcessor(config);
person Andy    schedule 27.04.2020
comment
конечно, это плохая практика, когда классы приложений зависят от IConfiguration. Но как только конфигурация становится сложной, просто привязки недостаточно. Я реализовал систему проверки для каждого класса конфигурации, чтобы вызывать ошибки при запуске. И этот код нужно было пройти модульное тестирование. :-) - person monty; 27.04.2020

Вы можете использовать следующую технику, чтобы имитировать IConfiguration.GetValue<T>(key) метод расширения.

var configuration = new Mock<IConfiguration>();
var configSection = new Mock<IConfigurationSection>();

configSection.Setup(x => x.Value).Returns("fake value");
configuration.Setup(x => x.GetSection("MySection")).Returns(configSection.Object);
//OR
configuration.Setup(x => x.GetSection("MySection:Value")).Returns(configSection.Object);
person Serj    schedule 19.03.2020
comment
Этот ответ больше относится к этому (закрытому) вопросу: stackoverflow.com/questions/43618686/ Но я не смог найти ничего другого, связанного с этой темой, с рабочим подходом Moq. Многие люди спрашивали об этом, но единственные ответы, которые вы можете найти, связаны с ConfigurationBuilder. Если бы вопрос был открытым, я бы разместил его там. - person Serj; 19.03.2020