【发布时间】:2012-03-18 04:04:12
【问题描述】:
我被困在我不知道如何模拟的代码点:
ConfigurationManager.AppSettings["User"];
我必须模拟 ConfigurationManager,但我不知道,我正在使用 Moq。
有人可以给我小费吗?谢谢!
【问题讨论】:
标签: c# unit-testing moq
我被困在我不知道如何模拟的代码点:
ConfigurationManager.AppSettings["User"];
我必须模拟 ConfigurationManager,但我不知道,我正在使用 Moq。
有人可以给我小费吗?谢谢!
【问题讨论】:
标签: c# unit-testing moq
我担心我需要回忆我说过的话。 ConfigurationManager.AppSettings 偶尔表现得很奇怪,就像它不会总是立即产生刚刚写入的值。因此,我们的构建机器上出现了零星的单元测试失败。我不得不重写我的代码以使用包装器,在通常情况下返回 ConfigurationManager.AppSettings,在单元测试中返回测试值。
设置你需要的怎么样?因为,我不想模拟 .NET,难道我...?
System.Configuration.ConfigurationManager.AppSettings["myKey"] = "myVal";
您可能应该事先清除 AppSettings 以确保应用只看到您想要的内容。
【讨论】:
实现此目标的另一种方法是提供您自己的IConfiguration,从您希望从中提取的任何文件中提取,如下所示:
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).Build();
现在,只要您在这个 JSON 文件中有测试所需的值,就可以很容易地覆盖和更改值。
【讨论】:
我相信解决这个问题的一种标准方法是使用 facade 模式来包装配置管理器,然后您就可以控制一些松散耦合的东西。
因此您将包装 ConfigurationManager。比如:
public class Configuration: IConfiguration
{
public User
{
get
{
return ConfigurationManager.AppSettings["User"];
}
}
}
(您可以从配置类中提取一个接口,然后在代码中的任何地方使用该接口) 然后你只需模拟 IConfiguration。您可能能够以几种不同的方式实现外观本身。上面我选择只包装各个属性。您还可以获得使用强类型信息而不是弱类型哈希数组的附带好处。
【讨论】:
var configurationMock = new Mock<IConfiguration>(); 和设置:configurationMock.SetupGet(s => s.User).Returns("This is what the user property returns!");
我认为编写自己的 app.config 提供程序是一项简单的任务,并且比其他任何事情都更有用。特别是您应该避免使用任何假货,例如垫片等,因为一旦您使用它们,“编辑并继续”就不再有效。
我使用的提供程序如下所示:
默认情况下,它们从 App.config 获取值,但对于单元测试,我可以覆盖所有值并在每个测试中独立使用它们。
不需要任何接口或一次又一次地实现它。我有一个实用程序 dll,并在许多项目和单元测试中使用这个小助手。
public class AppConfigProvider
{
public AppConfigProvider()
{
ConnectionStrings = new ConnectionStringsProvider();
AppSettings = new AppSettingsProvider();
}
public ConnectionStringsProvider ConnectionStrings { get; private set; }
public AppSettingsProvider AppSettings { get; private set; }
}
public class ConnectionStringsProvider
{
private readonly Dictionary<string, string> _customValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string this[string key]
{
get
{
string customValue;
if (_customValues.TryGetValue(key, out customValue))
{
return customValue;
}
var connectionStringSettings = ConfigurationManager.ConnectionStrings[key];
return connectionStringSettings == null ? null : connectionStringSettings.ConnectionString;
}
}
public Dictionary<string, string> CustomValues { get { return _customValues; } }
}
public class AppSettingsProvider
{
private readonly Dictionary<string, string> _customValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string this[string key]
{
get
{
string customValue;
return _customValues.TryGetValue(key, out customValue) ? customValue : ConfigurationManager.AppSettings[key];
}
}
public Dictionary<string, string> CustomValues { get { return _customValues; } }
}
【讨论】:
您是否考虑过使用存根代替模拟? AppSettings 属性是 NameValueCollection:
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
// Arrange
var settings = new NameValueCollection {{"User", "Otuyh"}};
var classUnderTest = new ClassUnderTest(settings);
// Act
classUnderTest.MethodUnderTest();
// Assert something...
}
}
public class ClassUnderTest
{
private readonly NameValueCollection _settings;
public ClassUnderTest(NameValueCollection settings)
{
_settings = settings;
}
public void MethodUnderTest()
{
// get the User from Settings
string user = _settings["User"];
// log
Trace.TraceInformation("User = \"{0}\"", user);
// do something else...
}
}
好处是实现更简单,并且在您真正需要之前不依赖 System.Configuration。
【讨论】:
IConfiguration 包装配置管理器可能级别太高,并且您可能会错过由于错误的配置值解析等原因而存在的错误。另一方面,像 LosManos 建议的那样直接使用 ConfigurationManager.AppSettings 是一个太多的实现细节,更不用说它会对其他测试产生副作用,并且不能在没有手动同步的情况下用于并行测试运行(因为 NameValueConnection 不是线程安全)。
我正在使用 AspnetMvc4。刚才我写了
ConfigurationManager.AppSettings["mykey"] = "myvalue";
在我的测试方法中,它运行良好。
说明:测试方法在应用程序设置取自的上下文中运行,通常是web.config 或myapp.config。 ConfigurationsManager 可以访问此应用程序全局对象并对其进行操作。
但是:如果您有一个测试运行程序并行运行测试,这不是一个好主意。
【讨论】:
ConfigurationManager.AppSettings 是一个NameValueCollection,它不是线程安全的,因此在没有适当同步的情况下使用它进行并行测试无论如何都不是一个好主意。否则,您只需在您的 TestInitialize / ctor 中调用 ConfigurationManager.AppSettings.Clear() 就可以了。
您可以使用 shims 将 AppSettings 修改为自定义的 NameValueCollection 对象。以下是如何实现此目的的示例:
[TestMethod]
public void TestSomething()
{
using(ShimsContext.Create()) {
const string key = "key";
const string value = "value";
ShimConfigurationManager.AppSettingsGet = () =>
{
NameValueCollection nameValueCollection = new NameValueCollection();
nameValueCollection.Add(key, value);
return nameValueCollection;
};
///
// Test code here.
///
// Validation code goes here.
}
}
您可以在Isolating Code Under Test with Microsoft Fakes 上阅读有关垫片和假货的更多信息。希望这可以帮助。
【讨论】:
也许这不是您需要完成的,但您是否考虑过在您的测试项目中使用 app.config? 因此,ConfigurationManager 将获取您放入 app.config 中的值,您无需模拟任何内容。 这个解决方案很适合我的需求,因为我从不需要测试“变量”配置文件。
【讨论】:
Web.config 中提取。在测试期间,从app.config 中提取一些众所周知的值是非常有效的。单元测试只需要确保拉出“cluster1”时的条件有效;在这种情况下,只有 4 个不同的集群。
那是一个静态属性,Moq 被设计为 Moq 实例方法或类,可以通过继承来模拟。换句话说,起订量在这里对您没有任何帮助。
为了模拟静态,我使用了一个名为Moles 的工具,它是免费的。还有其他框架隔离工具,比如 Typemock 也可以做到这一点,尽管我相信这些是付费工具。
当涉及到静态和测试时,另一种选择是自己创建静态状态,尽管这通常会出现问题(我想在你的情况下会是这样)。
最后,如果隔离框架不是一种选择,并且您致力于这种方法,那么 Joshua 提到的外观是一种很好的方法,或者一般而言,您将客户端代码从业务中分离出来的任何方法您用来测试的逻辑。
【讨论】: