Модульное тестирование пользовательской проверки на основе отложенной проверки IOptions. NET Core 1.1 и выше - PullRequest
0 голосов
/ 22 января 2020

Это не вопрос, а пример, который я попробовал, где вопросы не задавались. В случае, если кто-нибудь еще попробует этот тип идиотного c модульного тестирования в будущем, это мои выводы:

При попытке реализовать тщательную проверку, поскольку в настоящее время она не поддерживается. NET Core 3.1, но, как указано в документации в нижней части раздела https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1#options -post-configuration :

Готовность к проверке (быстрый запуск при запуске) рассматривается в будущем выпуске.

Вы не можете программно проверить ленивую проверку доступа к рассматриваемой опции, если вы внедрили пользовательскую нетерпеливую проверку.

Вот что я сделал:

Создан класс конфигурации

public class TestOptions : IValidateObject // for eager validation config
{
    [Required]
    public string Prop { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (string.IsNullOrEmpty(this.Prop))
            yield return new ValidationResult($"{nameof(this.Prop)} is null or empty.");
    }
}

В мою библиотеку, которую я тестирую, добавлена ​​конфигурация:

public static void AddConfigWithValidation(this IServiceCollection services, Action<TestOptions> options)
{
    var opt = new TestOptions();
    options(opt);

    // eager validation
    var validationErrors = opt.Validate(new ValidationContext(opt)).ToList();

    if (validationErrors.Any())
        throw new ApplicationException($"Found {validationErrors.Count} configuration error(s): {string.Join(',', validationErrors)}");

    // lazy validation with validate data annotations from IOptions
    services.AddOptions<TestOptions>()
        .Configure(o =>
        {
            o.Prop = opt.Prop
        })
        .ValidateDataAnnotations();
}

И тест выглядит следующим образом

public class MethodTesting
{
    private readonly IServiceCollection _serviceCollection;

    public MethodTesting()
    {
        _serviceCollection = new ServiceCollection();
    }

    // this works as it should
    [Fact] 
    public void ServiceCollection_Eager_Validation()
    {
        var opt = new TestOptions { Prop = string.Empty };
        Assert.Throws<ApplicationException>(() => _serviceCollection.AddConfigWithValidation(o =>
        {
            o.Prop = opt.Prop
        });
    }

    // this does not work
    [Fact]
    public void ServiceCollection_Lazy_Validation_Mock_Api_Start()
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("settings.json", optional: false, reloadOnChange: true);

        _configuration = builder.Build();

        var opt = _configuration.GetSection(nameof(TestOptions)).Get<TestOptions>();

        _serviceCollection.AddConfigWithValidation(o =>
        {
            o.Prop = opt.Prop
        });

        // try to mock a disposable object, sort of how the API works on subsequent calls
        using (var sb = _serviceCollection.BuildServiceProvider())
        {
            var firstValue = sb.GetRequiredService<IOptionsSnapshot<TestOptions>>().Value;
            firstValue.Should().BeEquivalentTo(opt);
        }

        // edit the json file programmatically, trying to trigger a new IOptionsSnapshot<>
        var path = $"{Directory.GetCurrentDirectory()}\\settings.json";

        var jsonString = File.ReadAllText(path);

        var concreteObject = Newtonsoft.Json.JsonConvert.DeserializeObject<TestObject>(jsonString);

        concreteObject.TestObject.Prop = string.Empty;

        File.WriteAllText(path, Newtonsoft.Json.JsonConvert.SerializeObject(concreteObject));

        using (var sb = _serviceCollection.BuildServiceProvider())
        {
            // this does not work, as the snapshot is still identical to the first time it is pulled
            Assert.Throws<OptionsValidationException>(() => _serviceCollection.BuildServiceProvider().GetRequiredService<IOptionsSnapshot<TestOptions>>().Value);
        }
    }

    // this does not work as well
    [Fact]
    public void ServiceCollection_Lazy_Validation_Mock_Api_Start_With_Direct_Prop_Assignation()
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("settings.json", optional: false, reloadOnChange: true);

        _configuration = builder.Build();

        var opt = _configuration.GetSection(nameof(TestOptions)).Get<TestOptions>();

        _serviceCollection.AddConfigWithValidation(o =>
        {
            o.Prop = opt.Prop
        });

        using (var sb = _serviceCollection.BuildServiceProvider())
        {
            var firstValue = sb.GetRequiredService<IOptionsSnapshot<TestOptions>>().Value;
            firstValue.Should().BeEquivalentTo(opt);
        }

        var prop = _configuration["TestOptions:Prop"];

        _configuration["TestOptions:Prop"] = string.Empty;

        // this returns a new value
        var otherProp = _configuration["TestOptions:Prop"];

        using (var sb = _serviceCollection.BuildServiceProvider())
        {
            // this does not work, the snapshot is not yet modified, however, calling _configuration.GetSection(nameof(TestOptions)).Get<TestOptions>(); does return the new TestOptions.

            Assert.Throws<OptionsValidationException>(() => _serviceCollection.BuildServiceProvider().GetRequiredService<IOptionsSnapshot<TestOptions>>().Value);
        }

    }

    public class TestObject
    {
        public TestOptions TestOptions { get; set; }
    }

Мои настройки. json выглядело так:

{
    "TestOptions": {
        "Prop": "something"
    }
}

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

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

Если у вас есть предложения, вопросы или вы хотите обсудить данную тему, не стесняйтесь использовать раздел комментариев

Ответы [ 2 ]

0 голосов
/ 14 марта 2020

Для нетерпеливой проверки я наткнулся на это сообщение на github (не могу поверить в это, но, похоже, это помогает)

Я использую следующим образом .. .

    public static IServiceCollection AddOptionsWithEagerValidation<TOptions, TOptionsValidator>(this IServiceCollection services,
            Action<TOptions> configAction,
            ILogger<ServiceCollection>? logger = default)
        where TOptions : class, new()
        where TOptionsValidator : class, IValidator, new()
    {
        services
            .AddOptions<TOptions>()
            .Configure(configAction)
            .Validate(x =>
            {
                return ValidateConfigurationOptions<TOptions, TOptionsValidator>(x, logger);
            })
            .ValidateEagerly();

        return services;
    }

Я делаю некоторые пользовательские вещи во время Configure, а затем выполняю свою собственную проверку с использованием Fluent Validation в Validate. ValidateEagerly заставляет IStatupFilter рано проверять параметры.

0 голосов
/ 27 января 2020

Похоже, я нашел что-то, что может удовлетворить ленивую притчу о валидации, которая имеет жадную валидацию. Обратите внимание, что IValidatableObject и IValidateOptions для активной проверки не имеют значения, поэтому, пожалуйста, используйте все, что подходит вам лучше всего!

Решение:

public static void AddConfigWithValidation(this IServiceCollection services, IConfiguration config)
{
    // lazy validation
    services.Configure<TestOptions>(config.GetSection(nameof(TestOptions))).AddOptions<TestOptions>().ValidateDataAnnotations();

    var model = config.GetSection(nameof(TestOptions)).Get<TestOptions>();

    // eager validation
    var validationErrors = model.Validate(new ValidationContext(model)).ToList();

    if (validationErrors.Any())
        throw new ApplicationException($"Found {validationErrors.Count} configuration error(s): {string.Join(',', validationErrors)}");
}

И в методе теста:

[Fact]
public void ServiceCollection_Lazy_Validation_Mock_Api_Start()
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("settings.json", optional: false, reloadOnChange: true);

    _configuration = builder.Build();

    var opt = _configuration.GetSection(nameof(TestOptions)).Get<TestOptions>();

    _serviceCollection.AddConfigWithValidation(_configuration);

    var firstValue = _serviceCollection.BuildServiceProvider().GetRequiredService<IOptionsSnapshot<TestOptions>>().Value;

    firstValue.Should().BeEquivalentTo(opt);

    // edit the json file programmatically, trying to trigger a new IOptionsSnapshot<>
    var path = $"{Directory.GetCurrentDirectory()}\\settings.json";

    var jsonString = File.ReadAllText(path);

    var concreteObject = Newtonsoft.Json.JsonConvert.DeserializeObject<TestObject>(jsonString);

    concreteObject.TestObject.Prop = string.Empty;

    File.WriteAllText(path, Newtonsoft.Json.JsonConvert.SerializeObject(concreteObject));

    _configuration = builder.Build(); // rebuild the config builder

    System.Threading.Thread.Sleep(1000); // let it propagate the change

    // error is thrown, lazy validation is triggered.
    Assert.Throws<OptionsValidationException>(() => _serviceCollection.BuildServiceProvider().GetRequiredService<IOptionsSnapshot<TestOptions>>().Value);
}

Теперь это работает правильно, и отложенная проверка запускается.

Обратите внимание, что я попытался имитировать c их реализацию для прослушивания изменений IConfiguration, но она не сработала.

...