如何单元测试方法调用 IConfiguration.Get<T> 扩展
How to Unit Test Method Calling IConfiguration.Get<T> extension
我有一个非常简单的方法需要进行单元测试。
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
。我无法更改该静态方法的实现。
我在想,也许最简单的方法是创建内存配置提供程序?但是我不知道我是否可以在不将其绑定到网络主机的情况下创建一个。
[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 创建了一个 mock 并模仿了 ConfigBinder 的行为
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
为其编译添加了这两个命名空间
配置模块独立于虚拟主机相关功能。
您应该能够创建内存配置以进行测试,而无需将其绑定到 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
}
在我看来,这将接近于集成测试,但理想情况下,您只是使用依赖于框架的功能来隔离扩展方法的测试。
解决问题的一种稍微不同的方法,避免 Mock 和大量设置噪音:
InMemoryConfiguration 几乎 给了我我需要的东西,所以我扩展了它,这样你就可以在构建配置后修改值(我遇到的情况我不知道所有的模拟构建配置时的值)
https://gist.github.com/martinsmith1968/9567de76d2bbe537af05d76eb39b1162
底部的单元测试显示用法
大多数模拟库(Moq、FakeItEasy 等)无法模拟扩展方法。
因此,您必须 'fill' 您的 IConfiguration 的方式 IConfiguration.Get<T>
returns T.Nkoski 答案的实例适用于 lot 的场景,但如果您 需要 测试调用 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>()
}
}
}
我有一个非常简单的方法需要进行单元测试。
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
。我无法更改该静态方法的实现。
我在想,也许最简单的方法是创建内存配置提供程序?但是我不知道我是否可以在不将其绑定到网络主机的情况下创建一个。
[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 创建了一个 mock 并模仿了 ConfigBinder 的行为
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
为其编译添加了这两个命名空间
配置模块独立于虚拟主机相关功能。
您应该能够创建内存配置以进行测试,而无需将其绑定到 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
}
在我看来,这将接近于集成测试,但理想情况下,您只是使用依赖于框架的功能来隔离扩展方法的测试。
解决问题的一种稍微不同的方法,避免 Mock 和大量设置噪音:
InMemoryConfiguration 几乎 给了我我需要的东西,所以我扩展了它,这样你就可以在构建配置后修改值(我遇到的情况我不知道所有的模拟构建配置时的值)
https://gist.github.com/martinsmith1968/9567de76d2bbe537af05d76eb39b1162
底部的单元测试显示用法
大多数模拟库(Moq、FakeItEasy 等)无法模拟扩展方法。
因此,您必须 'fill' 您的 IConfiguration 的方式 IConfiguration.Get<T>
returns T.Nkoski 答案的实例适用于 lot 的场景,但如果您 需要 测试调用 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>()
}
}
}