在C# .NET Core单元测试项目中无法使用ConfigurationManager读取app.config文件

73
我创建了一个简单的单元测试项目来读取 app.config 文件,目标框架是 Core 2.0。我还创建了一个 Core 2.0 控制台应用程序,以进行自我检查,确保我没有做任何奇怪的事情(在 .NET 4.6.1 单元测试项目中,同样的测试按预期通过)。
控制台应用程序可以成功读取 app.config,但是单元测试方法失败了,我无法弄清原因。两者都使用相同的 app.config 副本(未添加为链接),并且都安装了 System.Configuration.ConfigurationManager v4.4.1 NuGet 包。 App.config 文件内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="Test1" value ="This is test 1."/>
    <add key="Test2" value ="42"/>
    <add key="Test3" value ="-42"/>
    <add key="Test4" value="true"/>
    <add key="Test5" value="false"/>
    <add key="Test6" value ="101.101"/>
    <add key="Test7" value ="-1.2345"/>
  </appSettings>
</configuration>

单元测试

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Configuration;

namespace ConfigTest
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod()]
        public void ConfigTest()
        {
            foreach (string s in ConfigurationManager.AppSettings.AllKeys)
            {
                System.Console.WriteLine(s);
                System.Diagnostics.Debug.WriteLine(s);
            }

            //AllKeys.Length is 0? Should be 7...
            Assert.IsTrue(ConfigurationManager.AppSettings.AllKeys.Length == 7);
        }
    }
}

控制台应用程序

using System;
using System.Configuration;

namespace ConfigTestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            foreach (string s in ConfigurationManager.AppSettings.AllKeys)
            {
                Console.WriteLine(s);
                System.Diagnostics.Debug.WriteLine(s);
            }

            //Outputs 7 as expected
            Console.WriteLine(ConfigurationManager.AppSettings.AllKeys.Length);
        }
    }
}  

鉴于我对整个.NET Core世界还很陌生,我在这里做的事情完全错误吗?我现在感觉有些疯狂...

两个带有app.config的项目


2
你确定正确的.config文件已经链接到测试项目,并且测试项目没有自己读取的.config文件吗? - Paweł Łukasik
ConfigurationManager.AppSettings[0]; 这段代码可以工作吗? - MichaelEvanchik
@PawełŁukasik - 整个解决方案目录中我只有两个配置文件,就是你在上面看到的那些(除了生成的*.dll.configs)。属性窗口中列出的完整路径是我所期望的。 - Broots Waymb
@MichaelEvanchik - 不是。参数超出范围异常。 - Broots Waymb
我刚刚进行了一项快速测试(虽然不是在 .net core 上),如果将 app.config 作为链接添加到测试项目中,它可以正常工作! - Paweł Łukasik
显示剩余5条评论
13个回答

61
浏览github的问题评论,我发现了一个可以放在msbuild文件中的解决方法...
<Target Name="CopyCustomContent" AfterTargets="AfterBuild">
  <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\testhost.dll.config" />
</Target>

这使得在将配置数据移植到JSON配置文件之前,在 .NET Core 下验证现有测试变得更加容易。

编辑

如果在 Resharper 下运行,则先前的答案无效,因为 Resharper 会代理该程序集,因此您需要

<Target Name="CopyCustomContent" AfterTargets="AfterBuild">
  <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\ReSharperTestRunner64.dll.config" />
</Target>

编辑 #2

如果您正在运行VS2022和Resharper,则需要通过从输出文件中删除64来修改上述内容,如下所示:

<Target Name="CopyCustomContent" AfterTargets="AfterBuild">
  <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\ReSharperTestRunner.dll.config" />
</Target>

很可能是因为Visual Studio现在默认以64位模式运行。


5
不确定是否仅适用于VS 2022(现在是64位),但在编写本文时(2022年4月),ReSharper和VS的版本需要将配置文件命名为ReSharperTestRunner.dll.config(没有64)才能正常工作。希望有人会发现这个有用。 - Mike Loux
@MikeLoux 我会检查一下,如有必要会进行更新。 - Paul Hatcher

38

如果您检查对 ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); 的调用结果,它应该告诉您在运行该程序集的单元测试时所需配置文件的位置。

我发现,与其拥有一个 app.config 文件不同,ConfigurationManager 正在寻找一个 testhost.dll.config 文件。

这是针对一个项目的目标为 netcoreapp2.1,并引用了 Microsoft.NET.Test.SdkNUnit 3.11Nunit3TestAdapter 3.12.0


1
获取 app.config 文件以测试 testhost.dll.config 的方法,请参见下面的答案。 - Paul Hatcher
5
注意:为了使其正常工作,我不仅需要将文件属性“复制到输出目录”设置为“始终复制”,还需要将其从app.config重命名为testhost.dll.config。感谢提供的重命名提示! - user1568891
1
这是我用来确定当前版本的R#正在寻找ReSharperTestRunner.dll.config而不是(再也不是)ReSharperTestRunner64.dll.config的方法。 - Mike Loux

25

.CORE 3.1 为了找出正在使用的dll.config文件,我通过添加这行代码进行了调试,并查看其值。


string path = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath;

然后我发现resharper正在使用testhost.dll.config,而VStest则在使用testhost.x86.dll.config。我需要在项目文件中添加以下行。

  <Target Name="CopyCustomContent" AfterTargets="AfterBuild">
    <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\testhost.dll.config" />
    <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\testhost.x86.dll.config" />
  </Target>

4
testhost.x86.dll.config也是我Net Core 3.0项目所需的名称。将app.config重命名为testhost.x86.dll.config,并将“复制到输出目录”属性设置为始终复制也可以实现。 - Austin

13

我在使用xunit测试时遇到了同样的问题,通过使用ConfigurationManager中的Configuration实例解决了这个问题。在展示另一种解决方法之前,我先介绍了在核心框架(但不是单元测试)中的静态(常规)方式。

        var appSettingValFromStatic = ConfigurationManager.AppSettings["mySetting"];
        var appSettingValFromInstance = ConfigurationManager.OpenExeConfiguration(Assembly.GetExecutingAssembly().Location).AppSettings.Settings["mySetting"].Value;

这里还有一个类似的/相关的问题。如果有人需要获取某个部分,可以做类似的事情,尽管应用程序配置中的类型必须更改:

<configSections>
    <section name="customAppSettingsSection" type="System.Configuration.AppSettingsSection"/>
    <section name="customNameValueSectionHandlerSection" type="System.Configuration.NameValueSectionHandler"/>
</configSections>

<customAppSettingsSection>
    <add key="customKey" value="customValue" />
</customAppSettingsSection>

<customNameValueSectionHandlerSection>
    <add key="customKey" value="customValue" />
</customNameValueSectionHandlerSection>

抓取章节的代码:

        var valFromStatic = ((NameValueCollection)ConfigurationManager.GetSection("customNameValueSectionHandlerSection"))["customKey"];
        var valFromInstance = ((AppSettingsSection)ConfigurationManager.OpenExeConfiguration(Assembly.GetExecutingAssembly().Location).GetSection("customAppSettingsSection")).Settings["customKey"].Value;

我觉得我也很疯狂,我知道核心配置有更新的方法,但如果想要在跨平台上做一些事情,这是我唯一知道的方法。如果有人有替代方案,我会非常感兴趣。


8

对于我的混合.NET-Core和.NET-Framework项目,我将以下内容添加到单元测试全局设置:

#if NETCOREAPP
using System.Configuration;
using System.IO;
using System.Reflection;
#endif

...

// In your global setup:
#if NETCOREAPP
    string configFile = $"{Assembly.GetExecutingAssembly().Location}.config";
    string outputConfigFile = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath;
    File.Copy(configFile, outputConfigFile, true);
#endif

这将配置文件复制到输出路径 testhost.dll.config,但应该足够弹性以应对测试框架未来的更改。

或者您可以复制到下面,其效果相同:

string outputConfigFile = Path.Combine(Path.GetDirectoryName(configFile), $"{Path.GetFileName(Assembly.GetEntryAssembly().Location)}.config");

感谢 @stop-cran 和 @PaulHatcher 提供的解决方案,这是两个方案的结合。


我收到了未经授权的访问错误。 - Brian Kriesel

6
没有任何一个答案提供可行的解决方法,当你处理直接访问静态ConfigurationManager属性(如AppSettingsConnectionStrings)的代码时。事实上,目前不可能。您可以阅读此处的讨论以了解原因:https://github.com/dotnet/corefx/issues/22101。有谈论在此处实现支持:https://github.com/Microsoft/vstest/issues/1758。在我看来,支持这种情况是有意义的,因为它已经在.NET Framework上工作,并且System.Configuration.ConfigurationManager现在是一个.NET Standard 2.0库。

1
我完全同意你最后一个观点。当然,这不是“新/首选”的做事方式,但在.NET中它确实起作用,并且在那个时候,它在这里不起作用对我来说似乎非常奇怪。 - Broots Waymb

5
当我们回答这样深入研究和表达清晰的问题时,应该更好地假设提问者是一位知情且智慧的存在。我们不应该向他们发号施令地讲述关于新的、优秀的方法来编写大量的样板代码以解析各种JSON等数据,而是应该专注于简明扼要地回答问题。
既然原帖作者已经使用了System.Configuration来访问设置,他们已经知道如何到达这一点。唯一缺少的就是一个小细节:将此行添加到后期构建事件中:
copy $(OutDir)<appname>.dll.config $(OutDir)testhost.dll.config

在这里,<appname> 是被单元测试的项目名称。

我赞扬仍在使用(原本不起眼但可行的)app.config 实现的所有人,因为这样做可以保护我们和客户对技术的投资,而不是重复造轮子。阿门。


4

现在有一种方法可以在运行时设置预期配置文件的名称,您可以为当前应用程序域设置APP_CONFIG_FILE数据。

我创建了以下SetUpFixture来自动完成此操作:

[SetUpFixture]
public class SetUpFixture
{
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        var testDllName = Assembly.GetAssembly(GetType())
                                  .GetName()
                                  .Name;
        var configName = testDllName + ".dll.config";
        AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", configName);
    }
}

相关的 GitHub 讨论如下:

4
一种可行但不太正规的方法是将配置文件复制到与入口程序集相同的文件夹中,无论它是什么:
[SetUpFixture]
public class ConfigKludge
{
    [OneTimeSetUp]
    public void Setup() =>
        File.Copy(
            Assembly.GetExecutingAssembly().Location + ".config",
            Assembly.GetEntryAssembly().Location + ".config",
            true);

    [OneTimeTearDown]
    public void Teardown() =>
        File.Delete(Assembly.GetEntryAssembly().Location + ".config");
}

除了添加这个类,唯一需要做的就是在测试项目中包含app.config文件(不需要复制任何选项)。在构建步骤中,应将该文件复制到输出文件夹中,作为<your test project name>.dll.config,因为这是一种默认逻辑。
请注意OneTimeSetUpAttribute的文档:

摘要:标识一个方法,在运行任何子测试之前调用一次以执行设置。

虽然它应该适用于单个项目的并行测试运行,但同时运行两个测试项目时可能会出现明显的问题,因为配置文件会被覆盖。
然而,它仍适用于容器化的测试运行,例如Travis

2
ConfigurationManager API 仅使用当前运行的应用程序的配置。在单元测试项目中,这意味着使用测试项目的 app.config,而不是控制台应用程序的 app.config。
.NET Core 应用程序不应使用 app.config 或 ConfigurationManager,因为它是一个传统的“完整框架”配置系统。
考虑使用Microsoft.Extensions.Configuration来读取 JSON、XML 或 INI 配置文件。请参阅此文档:https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration

3
我有点困惑你第一段话的意思是什么。单元测试项目确实有它自己的 app.config。 - Broots Waymb
1
测试项目已经有了吗?我的意思是你可以在单元测试项目中添加一个app.config文件,看看AppSettings是否会显示出来。 - Martin Ullrich
1
在进行单元测试时,你的注意力应该集中在要测试的特定方法上,并且应该消除不必要的依赖关系。在这种情况下,尝试使用模拟和动态代理技术(使用 Microsoft Mole and Pex 或 moc)来处理 system.configuration 类。 - MichaelEvanchik
等待是完整框架测试一个“经典”的测试项目?还是一个新的SDK风格的测试项目?经典的可能会起作用,新的SDK风格测试不会有这个AFAIK,因为配置只适用于主机,这将是一个测试运行器可执行文件。 - Martin Ullrich
2
你最终解决了这个问题吗?看起来很傻,ConfigurationManager在框架、标准和核心中都能工作,但在单元测试中却不能。 - Mario
显示剩余6条评论

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