基于IOptions .NET Core 1.1及更高版本的惰性验证之上的单元测试定制急切验证

问题描述 投票:0回答:1

这不是问题,而是我尝试进行的案例研究,未提出任何问题。如果将来有人尝试这种愚蠢的单元测试,这些是我的发现:

[尝试执行急切验证,因为.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; }
    }

我的settings.json看起来像:

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

作为测试进行启动和运行的解决方案,是添加一个可选参数或带有可选参数的重载方法,以强制或不要求进行急切的验证,并测试当停用急切的方法时,惰性验证可以正常工作。

[请注意,这不是完美的方法,但是是一种测试方法,适用于想要测试提供的选项来自已更新但未重新启动应用程序的人时,如何测试急切和懒惰的验证。] >

如果您有任何建议,问题或想要就此主题进行讨论,请随时使用评论部分

这不是问题,而是我尝试进行的案例研究,未提出任何问题。如果将来有人尝试这种愚蠢的单元测试,这些是我的发现:...

c# .net-core xunit asp.net-core-1.1 asp.net-core-3.1
1个回答
0
投票

好像我找到了可以满足懒惰的验证寓言的东西,它在其之上急切地进行了验证。请注意,IValidatableObject与IValidateOptions进行急切的验证没有区别,因此请使用最合适的方法!

© www.soinside.com 2019 - 2024. All rights reserved.