web-dev-qa-db-fra.com

Remplissez IConfiguration pour les tests unitaires

La configuration .NET Core permet à tant d'options d'ajouter des valeurs (variables d'environnement, fichiers json, arguments de ligne de commande).

Je ne peux tout simplement pas comprendre et trouver une réponse comment le remplir via le code.

J'écris des tests unitaires pour les méthodes d'extension aux configurations et je pensais que les remplir dans les tests unitaires via du code serait plus facile que de charger des fichiers json dédiés pour chaque test.

Mon code actuel:

  [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());

  }

Mettre à jour:

Un cas particulier est la "section vide" qui ressemblerait à ceci dans json.

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

Mise à jour 2:

Comme Matthew l'a souligné dans les commentaires, avoir une section vide dans le json donne le même résultat que de ne pas avoir la section du tout. J'ai distillé un exemple et oui, c'est le cas. J'avais tort de m'attendre à un comportement différent.

Alors qu'est-ce que je fais et à quoi je m'attendais:

J'écris des tests unitaires pour 2 méthodes d'extension pour IConfiguration (en fait parce que la liaison des valeurs dans la méthode Get ... Settings ne fonctionne pas pour une raison quelconque (mais c'est un sujet différent). Ils ressemblent à ceci:

  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;
  }

Mon malentendu était que si je plaçais une section vide dans les paramètres d'application, la méthode IsService1Configured() retournerait true (ce qui est manifestement faux maintenant). La différence que j'attendais est d'avoir une section vide maintenant la méthode GetService1Settings() retourne null et non pas comme je m'y attendais le MyService1Settings Avec toutes les valeurs par défaut.

Heureusement, cela fonctionne toujours pour moi car je n'aurai pas de sections vides (ou maintenant je sais que je dois éviter ces cas). Ce n'est qu'un cas théorique que j'ai rencontré lors de l'écriture des tests unitaires.

Plus loin sur la route (pour ceux qui sont intéressés).

Pour quoi l'utilise-je? Activation/désactivation du service basé sur la configuration.

J'ai une application qui contient un service/certains services. Selon le déploiement, j'ai besoin d'activer/désactiver complètement les services. En effet, certains (configurations locales ou tests) n'ont pas un accès complet à une infrastructure complète (services d'assistance comme la mise en cache, les métriques ...). Et je le fais via les applications. Si le service est configuré (la section de configuration existe), il sera ajouté. Si la section de configuration n'est pas présente, elle ne sera pas utilisée.


Le code complet de l'exemple distillé est ci-dessous.

  • dans Visual Studio, créez une nouvelle API nommée WebApplication1 à partir des modèles (sans HTTPS et authentification)
  • supprimer la classe de démarrage et appsettings.Development.json
  • remplacez le code dans Program.cs par le code ci-dessous
  • maintenant dans appsettings.json, vous pouvez activer/désactiver les services en ajoutant/supprimant la section Service1 et 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

Est-ce que AddInMemoryCollection la méthode d'extension aiderait?

Vous pouvez y passer une collection de valeurs-clés: IEnumerable<KeyValuePair<String,String>> avec les données dont vous pourriez avoir besoin pour un test.

var builder = new ConfigurationBuilder();

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

La solution que j'ai choisie (qui répond au moins au titre de la question!) Consiste à utiliser un fichier de paramètres dans la solution testsettings.json et définissez-le sur "Copier toujours".

    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

Vous pouvez utiliser la technique suivante pour simuler la méthode d'extension 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