如何使用Moq模拟ConfigurationManager.AppSettings

147

我卡在这段代码上,不知道该如何进行模拟:

ConfigurationManager.AppSettings["User"];

我必须模拟 ConfigurationManager,但是我不知道该怎么做,我正在使用 Moq

有人可以给我一些提示吗?谢谢!

9个回答

222

我正在使用AspnetMvc4。刚才我写了:

ConfigurationManager.AppSettings["mykey"] = "myvalue";

在我的测试方法中,它表现得非常完美。

解释:测试方法在上下文中运行,其中应用程序设置来自于通常是 web.configmyapp.config 的文件。 ConfigurationsManager 可以访问这个应用程序全局对象并对其进行操作。

但是:如果您有一个并行运行测试的测试运行器,这不是一个好主意。


10
这真是一个聪明而简单的解决问题的方法!赞美它的简洁! - Navap
1
在大多数情况下,比起创建一个抽象来说要容易得多。 - Michael Clark
2
仅此而已????这种简洁的风格才是精髓,因为我一直在费尽心思地想如何测试这个特定的封闭类。 - Piotr Kula
8
ConfigurationManager.AppSettings 是一个 NameValueCollection,它不是线程安全的,因此如果没有适当同步,使用它进行并行测试不是一个好主意。否则,您可以在 TestInitialize/构造函数中调用 ConfigurationManager.AppSettings.Clear(),就可以避免这个问题了。 - Ohad Schneider
1
简单明了。到目前为止最好的答案! - znn
显示剩余4条评论

120

我相信一种标准的方法是使用外观模式(facade pattern)来包装配置管理器,这样你就可以得到一个松散耦合并且可控的东西。

那么你需要包装 ConfigurationManager。类似这样:

public class Configuration: IConfiguration
{
    public string User
    {
        get
        { 
            return ConfigurationManager.AppSettings["User"];
        }
    }
}

您可以从配置类中提取一个接口,然后在代码的各个地方使用该接口。然后,您只需模拟IConfiguration即可。您可能能够以几种不同的方式实现facade本身。上面我选择了仅包装单个属性的方法。您还可以获得强类型信息而不是弱类型哈希数组的附加好处。


8
这也是我的概念。但是我使用Castle DictionaryAdapter(Castle Core的一部分),它可以动态生成接口的实现。我之前写过这方面的文章:http://blog.andreloker.de/post/2008/09/05/Getting-rid-of-strings-(3)-take-your-app-settings-to-the-next-level.aspx(向下滚动到“解决方案”以查看我如何使用Castle DictionaryAdapter)。 - Andre Loker
这很棒,是一篇好文章。我得记住这个,以备将来之需。 - Joshua Enfield
我可能还要补充一点 - 根据你的纯粹主义和解释,这也可以被称为委托代理或适配器。 - Joshua Enfield
3
以上代码片段仅展示了如何像平常一样使用Moq。以下是未经测试的示例代码:var configurationMock = new Mock<IConfiguration>();,为设置准备:configurationMock.SetupGet(s => s.User).Returns("This is what the user property returns!"); - Joshua Enfield
当一个层依赖于IConfiguration并且您需要模拟IConfiguration时,可以使用此场景,但是如何测试IConfiguration的实现呢?如果在单元测试中调用ConfigurationManager.AppSettings["User"],那么这将不会测试该单元,而是测试从配置文件中获取哪些值,这不是一个单元测试。如果您需要检查实现,请参见@ http://zpbappi.com/testing-codes-with-configurationmanager-appsettings/。 - nkalfov

22

也许这不是您需要实现的内容,但您是否考虑在测试项目中使用app.config文件呢?这样,ConfigurationManager将获取您在app.config中设置的值,您就无需模拟任何东西。对于我的需要,这个解决方案非常有效,因为我从未需要测试“可变”的配置文件。


7
如果被测试的代码行为因配置值不同而发生变化,如果它不直接依赖于 AppSettings,则在测试时更容易进行。 - Andre Loker
2
这种做法是不好的,因为你从来没有测试其他可能的设置。Joshua Enfield的答案非常适合测试。 - mkaj
4
虽然有人反对这个答案,但我认为他们的立场有点笼统。在某些情况下,这是一个非常有效的答案,而且它实际上取决于你的需求。例如,假设我有4个不同的集群,每个集群都有一个不同的基本URL。这4个集群在运行时从包含项目的 "Web.config" 中拉出来。在测试期间,从 "app.config" 中拉取一些众所周知的值是非常有效的。单元测试只需要确保在拉取“cluster1”时的条件是正确的;在这种情况下只有4个不同的集群。 - Mike Perrenoud

14

您可以使用 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.        
    }
}

您可以在使用 Microsoft Fakes 隔离测试代码中了解有关 shim 和 fake 的更多信息。希望能对您有所帮助。


7
作者要求提供一个如何使用 Moq 的方法,而不是关于 MS Fakes 的内容。 - JPCF
6
这有什么不同之处?它通过从他的代码中移除数据依赖来实现模拟。使用 C# Fakes 是其中一种方法! - Zorayr

11

你有考虑过使用桩对象而不是模拟对象吗?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 时不依赖它。


3
我最喜欢这种方法。一方面,如Joshua Enfield所建议的,将配置管理器包装在IConfiguration中可能太高级,您可能会错过由于不良配置值解析等原因而存在的错误。另一方面,像LosManos建议的直接使用ConfigurationManager.AppSettings太多了实现细节,更不用说它可能对其他测试产生副作用,而且在并行测试运行时无法自动同步(因为NameValueCollection不是线程安全的)。 - Ohad Schneider

4

我担心我需要撤回我之前所说的话。 ConfigurationManager.AppSettings 会偶尔出现异常行为,就好像它不总是立即返回刚写入的值一样。因此我们在构建机器上遇到了零散的单元测试失败情况。我不得不重写我的代码来使用包装器,在通常情况下返回 ConfigurationManager.AppSettings 中的值,在单元测试中则返回测试值。

那你考虑只设置你需要的吗?毕竟,我不想模拟 .NET, 不是吗...?

System.Configuration.ConfigurationManager.AppSettings["myKey"] = "myVal";

你可能需要事先清理AppSettings,以确保应用程序只看到你想要的内容。


3
那是一个静态属性,Moq旨在通过继承模拟实例方法或类。换句话说,Moq在这里无法提供任何帮助。
对于模拟静态内容,我使用一个名为Moles的免费工具。还有其他框架隔离工具,如Typemock也可以做到这一点,但我认为这些都是付费工具。
当涉及到静态内容和测试时,另一个选择是自己创建静态状态,尽管这通常会有问题(就像我想象中你的情况一样)。
最后,如果隔离框架不是一个选项,并且您致力于这种方法,则Joshua提到的门面是一个不错的选择,或者任何一种将此客户端代码从您用于测试的业务逻辑中分离出来的方法。

1
另一种实现此目标的方法是提供自己的 IConfiguration,从任何你想要的文件中获取它,就像这样:
var builder = new ConfigurationBuilder()
         .SetBasePath(Directory.GetCurrentDirectory())
         .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).Build();

现在,只要您在此JSON文件中拥有所需的测试值,覆盖和更改这些值就非常容易。

0

我认为编写自己的app.config提供程序是一项简单的任务,比其他任何东西都更有用。特别是你应该避免使用任何伪造的东西,如shims等,因为一旦你使用它们,编辑和继续就不再起作用。

我使用的提供程序看起来像这样:

默认情况下,它们从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; } }
}

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