web-dev-qa-db-ja.com

ユニットテスト用の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());

  }
_

更新:

特別なケースの1つは、jsonでは次のようになる「空のセクション」です。

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

アップデート2:

マシューがコメントで指摘したように、jsonに空のセクションがあると、セクションがない場合と同じ結果になります。私は例を抽出しましたが、そうです。私は別の行動を期待して間違っていました。

だから私は何をし、私は何を期待しましたか:

IConfigurationの2つの拡張メソッドのユニットテストを書いています(実際には、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;
  }
_

私の誤解は、appsettingsに空のセクションを配置すると、IsService1Configured()メソッドがtrueを返すということでした(これは明らかに間違っています)。私が期待した違いは、空のセクションを持つことであり、GetService1Settings()メソッドがnullを返すようになり、すべてのデフォルト値で_MyService1Settings_を期待したようになりません。

幸いなことに、空のセクションがないため(または、これらのケースを回避する必要があることがわかったため)、これはまだ機能します。これは、単体テストの作成中に遭遇した理論上のケースの1つにすぎません。

さらに進んで(興味のある人のために)。

何に使うの?構成ベースのサービスのアクティブ化/非アクティブ化。

サービス/いくつかのサービスがコンパイルされたアプリケーションがあります。展開によっては、サービスを完全にアクティブ化/非アクティブ化する必要があります。これは、一部の(ローカルまたはテストのセットアップ)が完全なインフラストラクチャ(キャッシュ、メトリックなどのヘルパーサービス)に完全にアクセスできないためです。そして、私はappsettingsを介してそれを行います。サービスが構成されている場合(構成セクションが存在する場合)、サービスが追加されます。 configセクションが存在しない場合は使用されません。


抽出された例の完全なコードは次のとおりです。

  • visual Studioで、テンプレートからWebApplication1という名前の新しいAPIを作成します(HTTPSおよび認証なし)。
  • スタートアップクラスと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)));
      }
    }

  }
_
5
monty

AddInMemoryCollection 拡張メソッドは役に立ちますか?

キーと値のコレクションをそれに渡すことができます:IEnumerable<KeyValuePair<String,String>>テストに必要な可能性のあるデータ。

var builder = new ConfigurationBuilder();

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

私が行ったソリューション(少なくとも質問のタイトルに答えます!)は、ソリューションで設定ファイルを使用することです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;
        }
    }
0
noelicus

次の手法を使用して、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);
0
Serj