Заполните 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 дает тот же результат, что и отсутствие раздела вообще. Я привел пример, и да, это так. Я ошибся, ожидая другого поведения.

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

Я пишу модульные тесты для 2 методов расширения для 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 со всеми значениями по умолчанию.

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

Далее по дороге (для интересующихся).

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

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


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

  • в 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)));
    }
  }

}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
59
0
34 595
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Поможет ли метод расширения AddInMemoryCollection?

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

var builder = new ConfigurationBuilder();

builder.AddInMemoryCollection(new Dictionary<string, string>
{
     { "key", "value" }
});

Да, это сработает. Я обновил свой вопрос, чтобы отразить недостающую часть.

monty 03.04.2019 17:00
Ответ принят как подходящий

Вы можете использовать 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

Эквивалентные параметры командной строки:

dotnet <myapp.dll> -- --Key1=Value1 --Nested:Key1=NestedValue1 --Nested:Key2=NestedValue2

Да, это сработает. Я обновил свой вопрос, чтобы отразить недостающую часть.

monty 03.04.2019 17:00

Вы должны обновить свой вопрос, чтобы включить то, что вы ожидаете. Наличие пустого узла JSON приводит к тому же результату, что и отсутствие этого узла вообще.

Matthew 03.04.2019 17:04

действительно ты был прав. Пустой раздел вроде бы удален и не существует. Я добавил обновление 2 к своему вопросу с полным примером того, что я (ошибочно) ожидал и почему.

monty 04.04.2019 09:02

ну, модульный тест показал, что привязка конфигурации не удалась, потому что я только что определил получение для свойства, но не для набора.

monty 04.04.2019 10:40

@Matthew добавление json в ваш пример было бы полезно

Philippe 19.10.2020 02:38

Решение, которое я выбрал (которое, по крайней мере, отвечает на заголовок вопроса!), Это использовать файл настроек в решении 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;
    }
}

Привет, ребята, AddJsonFile кажется немного измененным на стороне источника .net 5.0: https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.extensions.c‌​onfiguration.jsoncon‌​figurationextensions‌​.addjsonfile? view = dotnet-plat-ext-5.0 # Microsoft_Extensions_Configuration_JsonConfigurationExtensio‌​ns_AddJsonFile_Micro‌​onsoft

Beyto 23.03.2021 14:09

Я пытаюсь использовать это с .net 6.0 в VS 2022, но получаю сообщение об ошибке ConfigurationBuilder does not contain AddJsonFile

Joe 22.01.2022 05:07

Вы можете использовать следующую технику, чтобы имитировать метод расширения 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);

Этот ответ больше относится к этому (закрытому) вопросу: stackoverflow.com/questions/43618686/… Но я не смог найти ничего другого, связанного с этой темой, с работающим подходом Moq. Многие люди спрашивали об этом, но единственные ответы, которые вы можете найти, связаны с ConfigurationBuilder. Если бы вышеуказанный вопрос был открытым, я бы разместил его там.

Serj 19.03.2020 18:32

Я предпочитаю, чтобы мои классы приложений не зависели от 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);

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

monty 27.04.2020 14:48

Другие вопросы по теме