如何对调用IConfiguration.Get<T>扩展方法进行单元测试

8

我有一个非常简单的方法需要进行单元测试。

public static class ValidationExtensions
{
    public static T GetValid<T>(this IConfiguration configuration)
    {
        var obj = configuration.Get<T>();
        Validator.ValidateObject(obj, new ValidationContext(obj), true);
        return obj;
    }
}

问题在于configuration.Get<T>是一个静态扩展方法,不属于IConfiguration。我不能改变该静态方法的实现。
我在思考,可能最简单的方法是创建一个内存配置提供程序?但我不知道是否可以在不绑定到Web主机的情况下创建一个。
4个回答

22
配置模块与 Web 主机相关的特性无关。您应该能够创建一个内存配置,以便在不需要绑定到 Web 主机的情况下进行测试。请查看以下示例测试。

The configuration module is independent of web host related features.

You should be able to create an in memory configuration to test against without the need to bind it to a web host.

Review the following example test

public class TestConfig {
    [Required]
    public string SomeKey { get; set; }
    [Required] //<--NOTE THIS
    public string SomeOtherKey { get; set; }
}

//...

[Fact]
public void Should_Fail_Validation_For_Required_Key() {
    //Arrange
    var inMemorySettings = new Dictionary<string, string>
    {
        {"Email:SomeKey", "value1"},
        //{"Email:SomeOtherKey", "value2"}, //Purposely omitted for required failure
        //...populate as needed for the test
    };

    IConfiguration configuration = new ConfigurationBuilder()
        .AddInMemoryCollection(inMemorySettings)
        .Build();

    //Act
    Action act = () => configuration.GetSection("Email").GetValid<TestConfig>();

    //Assert
    ValidationException exception = Assert.Throws<ValidationException>(act);
    //...other assertions of validation results within exception object
}

在我看来,这已经接近于集成测试了,但理想情况下,您应该仅使用框架相关功能来隔离扩展方法的测试。


谢谢,这个很好用,我可以将其扩展成一个辅助类,以便所有单元测试都可以使用它。 - leeroya

0

大多数模拟库(Moq、FakeItEasy等)无法模拟扩展方法。

因此,您必须以一种方式“填充”您的IConfiguration,使得 IConfiguration.Get<T> 返回T的实例。Nkoski的答案适用于许多场景,但如果您需要测试调用IConfiguration.Get<T> 的代码,则可以使用下面的示例:

using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Xunit;

public class TestClass {
    public class Movie
    {
        public string Name { get; set; }
        public decimal Rating { get; set; }
        public IList<string> Stars { get; set; } //it works with collections
    }

    [Fact]
    public void MyTest()
    {
        var movie = new Movie { 
            Name = "Some Movie",
            Rating = 9, 
            Stars = new List<string>{"Some actress", "Some actor"}
        };

        var movieAsJson = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(movie));
        using(var stream = new MemoryStream(movieAsJson))
        {
            var config = new ConfigurationBuilder().AddJsonStream(stream).Build();
            var movieFromConfig = config.Get<Movie>();
            //var sut = new SomeService(config).SomeMethodThatCallsConfig.Get<Movie>()
        }
    }
}

0
一种稍微不同的解决问题的方式,避免了Mock和大量的设置噪音:
InMemoryConfiguration几乎给了我所需的东西,因此我扩展了它,以便在构建配置后可以修改值(我所处的情况是,在构建配置时我不知道所有模拟值)。

https://gist.github.com/martinsmith1968/9567de76d2bbe537af05d76eb39b1162

底部的单元测试显示用法


-1
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        IConfiguration mock = new MockConfiguration();
        var simpleObject = mock.GetValid<SimpleObject>();
        Assert.AreEqual(simpleObject.MyConfigStr, "123");
    }
}

public class SimpleObject
{
    public string MyConfigStr { get; set; }
}


public class MockConfiguration : IConfiguration
{
    public IConfigurationSection GetSection(string key)
    {
        return new MockConfigurationSection()
        {
            Value = "123"
        };
    }

    public IEnumerable<IConfigurationSection> GetChildren()
    {
        var configurationSections = new List<IConfigurationSection>()
        {
            new MockConfigurationSection()
            {
                Value = "MyConfigStr"
            }
        };
        return configurationSections;
    }

    public Microsoft.Extensions.Primitives.IChangeToken GetReloadToken()
    {
        throw new System.NotImplementedException();
    }

    public string this[string key]
    {
        get => throw new System.NotImplementedException();
        set => throw new System.NotImplementedException();
    }
}

public class MockConfigurationSection : IConfigurationSection
{
    public IConfigurationSection GetSection(string key)
    {
        return this;
    }

    public IEnumerable<IConfigurationSection> GetChildren()
    {
        return new List<IConfigurationSection>();
    }

    public IChangeToken GetReloadToken()
    {
        return new MockChangeToken();
    }

    public string this[string key]
    {
        get => throw new System.NotImplementedException();
        set => throw new System.NotImplementedException();
    }

    public string Key { get; }
    public string Path { get; }
    public string Value { get; set; }
}

public class MockChangeToken : IChangeToken
{
    public IDisposable RegisterChangeCallback(Action<object> callback, object state)
    {
        return new MockDisposable();
    }

    public bool HasChanged { get; }
    public bool ActiveChangeCallbacks { get; }
}

public class MockDisposable : IDisposable
{
    public void Dispose()
    {
    }
}

创建了一个IConfiguration的模拟对象,并模仿ConfigBinder的行为。
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;

添加了这两个命名空间以便编译


这是我的第一次尝试,但不行。Get 是 ConfigurationBinder 的静态扩展方法的一部分,而不是 IConfiguration 的一部分。因此,Moq 无法帮助我。 - Etienne Charland

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接