简体   繁体   English

在 IOptions .NET Core 1.1 及更高版本的惰性验证之上单元测试自定义热切验证

[英]Unit Testing custom eager validation on top of lazy validation from IOptions .NET Core 1.1 and up

This is not a question but a case study that was tried on by me where questions have not been asked.这不是一个问题,而是我在没有提出问题的情况下尝试的案例研究。 In case anyone else tries this kind of idiotic unit testing in the future, these are my findings:万一其他人在未来尝试这种愚蠢的单元测试,这些是我的发现:

While trying to implement eager validation, as it is not supported currently by .NET Core 3.1, but as the documentation states at the bottom of the section https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1#options-post-configuration :在尝试实施急切验证时,因为 .NET Core 3.1 目前不支持它,但正如该部分底部的文档所述https://learn.microsoft.com/en-us/aspnet/core/fundamentals/配置/选项?view=aspnetcore-3.1#options-post-configuration

Eager validation (fail fast at startup) is under consideration for a future release.未来版本正在考虑紧急验证(启动时快速失败)。

You cannot test programmatically the lazy validation from accessing the option in question if you've implemented custom eager validation.如果您已经实现了自定义预热验证,则无法通过访问相关选项以编程方式测试延迟验证。

This is what I did:这就是我所做的:

Created config class创建的配置类

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.");
    }
}

Added the configuration in my lib that I'm testing:在我正在测试的库中添加了配置:

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

And the test looks like this测试看起来像这样

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

My settings.json looked like:我的 settings.json 看起来像:

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

A solution to get this up and running as a test, is to add an optional parameter or an overloaded method with an optional parameter that enforces or not eager validation and test that the lazy validation works properly when the eager is deactivated.将其启动并作为测试运行的解决方案是添加一个可选参数或一个带有可选参数的重载方法,该可选参数强制执行或不执行急切验证,并在停用急切验证时测试惰性验证是否正常工作。

Please note that this is not perfect, but a method of test for people who want to test how the eager and lazy validation can be tested when the options provided are from a source that gets updated but the apps do not get restarted.请注意,这并不完美,但这是一种测试方法,适用于那些想要测试当提供的选项来自已更新但应用程序未重新启动的源时如何测试急切和惰性验证的人。

If you have suggestions, questions or want to discuss on the subject at hand, feel free to use the comment section如果您有建议、问题或想就手头的主题进行讨论,请随时使用评论部分

Looks like I found something that can satisfy the lazy validation parable that has eager validation on top of it.看起来我找到了一些可以满足惰性验证寓言的东西,在它之上有急切的验证。 Please note that IValidatableObject vs IValidateOptions for eager validation does not make a difference, so please use whatever fits you best!请注意,用于急切验证的 IValidatableObject 与 IValidateOptions 没有区别,所以请使用最适合你的东西!

The solution:解决方案:

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

And in test method:并在测试方法中:

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

This now works correctly and the lazy validation is triggered.这现在可以正常工作并触发惰性验证。

Please note that I have tried to mimic their implementation for IConfiguration listening to change but it did not work.请注意,我试图模仿他们的 IConfiguration 监听更改的实现,但它没有用。

For eager validation, I stumbled across this post on github (can't take any credit for it, but it seems to do the trick)为了进行急切的验证,我在 github 上偶然发现了这篇文章(不能相信它,但它似乎可以解决问题)

I use as follows...我使用如下...

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

I do some custom stuff during Configure and then perform my own validation using Fluent Validation in Validate .我在Configure期间做了一些自定义的事情,然后在Validate中使用 Fluent Validation 执行我自己的验证。 ValidateEagerly causes the IStatupFilter to validate the options early. ValidateEagerly使IStatupFilter提前验证选项。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM