【问题标题】:How to Unit Test Method Calling IConfiguration.Get<T> extension如何单元测试方法调用 IConfiguration.Get<T> 扩展
【发布时间】:2019-03-13 04:40:47
【问题描述】:

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

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&lt;T&gt;是静态扩展方法,不属于IConfiguration。我无法更改该静态方法的实现。

我在想,也许最简单的方法是创建一个内存配置提供程序?但我不知道是否可以在不将其绑定到网络主机的情况下创建一个。

【问题讨论】:

    标签: c# unit-testing asp.net-core .net-core asp.net-core-configuration


    【解决方案1】:

    配置模块独立于网络主机相关功能。

    您应该能够创建一个内存配置来进行测试,而无需将其绑定到 Web 主机。

    查看以下示例测试

    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
    }
    

    在我看来,这接近于集成测试,但理想情况下,您只是使用依赖于框架的功能来隔离扩展方法的测试。

    【讨论】:

      【解决方案2】:

      解决问题的方式略有不同,避免了 Mock 和大量设置噪音:

      InMemoryConfiguration 几乎给了我我需要的东西,所以我扩展了它,以便您可以在构建配置后修改值(我当时的情况我不知道所有模拟值构建配置)

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

      底部的单元测试显示用法

      【讨论】:

        【解决方案3】:

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

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

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

        【讨论】:

          【解决方案4】:
          [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 的一部分。因此,起订量对我没有帮助。
          猜你喜欢
          • 1970-01-01
          • 2023-03-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-01-30
          • 1970-01-01
          • 2016-05-15
          相关资源
          最近更新 更多