Can't read app.config in C# .NET Core unit test project with ConfigurationManager
Asked Answered
S

13

73

I've created a simple unit test project to read an app.config file. Target framework is Core 2.0. I also created a Core 2.0 console app, to sanity-check myself to make sure I wasn't doing anything weird (same test passed as expected in a .NET 4.6.1 unit test project).

The console app reads the app.config fine, but the unit test method fails and I cannot figure out why. Both are using a copy of the same app.config (not added as a link) and both have the System.Configuration.ConfigurationManager v4.4.1 NuGet package installed.

The 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>

The Unit Test

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

The Console App

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

Given that I'm still pretty new to the whole .NET Core world, am I doing something totally incorrect here? I sort of just feel crazy at the moment...

Both projects with an app.config

Subjoinder answered 7/2, 2018 at 14:54 Comment(10)
are you sure that correct .config is linked w/ the test project and that test project doesn't have its own .config that you read in the test?Pedometer
ConfigurationManager.AppSettings[0]; work?Orgell
@PawełŁukasik - The only 2 configs I have in the entire solution directory are copies of what you see above (apart from the *.dll.configs that get generated). The full path as listed in the properties window is what I expect them to be.Subjoinder
@Orgell - Nope. ArgumentOutOfRangeException.Subjoinder
@MartinUllrich - The unit test finds 0 config items (it should find 7 - I'll edit the code to make that more clear). The app is finding all 7 as expected. Did I understand your question correctly?Subjoinder
I've just did a quick test (not on .net core though) and if you add app.config as link to the test project it works ok!Pedometer
@PawełŁukasik - Try on Core. Like I mentioned, a .NET framework unit test works just fine.Subjoinder
@BrootsWaymb I read that System.Configuration was removed from core, so how does it even compile for you? You have any additional code to the one you've pasted?Pedometer
#17580985Orgell
@Orgell - That seems slightly different. I'm not using connection strings, and they are definitely app.configs (not web.configs) included with the project.Subjoinder
M
63

Looking through the github issue's comments, I found a work around that can go in the msbuild file...

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

This makes it easier to verify existing tests under .NET Core before porting the configuration data over to json configuration files.

Edit

If running under Resharper, the previous answer doesn't work as Resharper proxies the assembly, so you need

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

Edit #2

If you're running VS2022 and Resharper, the above needs to be modified by removing the 64 on the output file, like so:

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

Most likely due to Visual Studio now running in 64 bit mode by default.

Mullite answered 29/4, 2019 at 11:51 Comment(3)
Not sure if this is specific to VS 2022 (which is now 64-bit), but for the version of ReSharper and VS at the time of this writing (Apr 2022), the config file had to be named ReSharperTestRunner.dll.config (no 64) for it to work. Hoping someone finds this useful.Oringas
@MikeLoux I'll check it out and update if necessaryMullite
This is also true for running tests in Rider. As of 2023.3.4 copying to ReSharperTestRunner.dll works.Arellano
M
38

If you check the result of the call to ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

It should tell you where the required configuration file should be while running unit tests for that assembly.

I found that instead of having an app.config file, ConfigurationManager was looking for a testhost.dll.config file.

This was for a project targeting netcoreapp2.1, with a reference to Microsoft.NET.Test.Sdk,NUnit 3.11 and Nunit3TestAdapter 3.12.0

Methylamine answered 18/1, 2019 at 17:50 Comment(3)
See answer below for a way to get the app.config file to testhost.dll.configMullite
Note: I had to set the file property "Copy to Output Directory" to "Copy Always" in addition to renaming it from app.config to testhost.dll.config for it to work. Thanks for the rename tip!Chloroform
This is what I used to determine that the current version of R# was looking for ReSharperTestRunner.dll.config and not (anymore) ReSharperTestRunner64.dll.config.Oringas
T
25

.CORE 3.1 To find out what dll.config file was being used, I debugged the test by adding this line and looking to see what the value is.

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

Then I found out resharper was using testhost.dll.config and VStest was using testhost.x86.dll.config. I needed to add the following lines to the project file.

  <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>
Treed answered 8/5, 2020 at 21:15 Comment(1)
testhost.x86.dll.config was also the name that I needed for my net core 3.0 project. This also worked with the rename the app.config -> testhost.x86.dll.config and setting the copy to output directory property to copy always.Olivarez
M
13

I came across the same issue with my xunit tests and solved it by using the instance of Configuration from ConfigurationManager. I put the static (normal) way it works in core, framework (but not unit tests) before I show the alternative way it works in all three:

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

And here is a similar/related issue. In case anyone needs to get a section you can do a similar thing, though the type must change in the app config:

<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>

Code to grab section:

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

I feel like I am also crazy, and I know there are newer ways of doing config in core, but if one wants to do something cross-platform this is the only way I know how. I'd be very interested if anyone has alternatives

Maria answered 21/6, 2018 at 22:9 Comment(0)
G
8

For my mixed .NET-Core & .NET-Framework project, I added the following to the unit test global setup:

#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

This copies the config file to the output path testhost.dll.config but should be resilient enough to account for future changes in the testing framework.

Or you can copy to below, which amounts to the same thing:

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

Credit to @stop-cran and @PaulHatcher's solutions, this is a combination of those two.

Graphitize answered 29/4, 2020 at 14:37 Comment(1)
I get an unauthorized access error.Ybarra
P
6

None of the answers given here provide a viable workaround when you're dealing with code accessing directly the static ConfigurationManager properties such as AppSettings or ConnectionStrings.

The truth is, it is not possible at the moment. You can read through the discussion here to understand why: https://github.com/dotnet/corefx/issues/22101

There is talk to implement the support for it here: https://github.com/Microsoft/vstest/issues/1758

In my opinion it makes sense to support this scenario since it's been working on the .NET Framework plus System.Configuration.ConfigurationManager is a .NET Standard 2.0 library now.

Porkpie answered 24/10, 2018 at 22:31 Comment(1)
I agree with your last point completely. Sure, it's not the "new/preferred" way of doing things, but it certainly worked in .NET and seemed incredibly strange to me at the time that it didn't work here.Subjoinder
T
5

When we answer such well-researched and well-articulated question, we should better assume that it is being asked by an informed and intelligent being. And instead of patronizing them with the obvious about new, great ways of writing tonnes of boilerplate code to parse all sort of JSON et al, being forced on us and shoved down our throat by know-betters, we should focus on answering to the point.

Since the OP is already using System.Configuration to access settings, they already know how to arrive at this point. The only thing that is missing is one little touch: adding this line to the post-build event:

copy $(OutDir)<appname>.dll.config $(OutDir)testhost.dll.config

where <appname> is the project being unit-tested.

I applaud everyone who is still using (originally lame but workable) implementation of app.config because doing so protects our and our clients' investment in technology instead of reinventing the wheel. Amen.

Trapani answered 27/12, 2020 at 20:50 Comment(0)
M
4

A hacky, but working way is to copy the config to the same folder as an entry assembly, whatever it is:

[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");
}

Apart from adding this class, the only thing to make it work is to include app.config file in test project (without any copying options). It should be copied to the output folder as <your test project name>.dll.config at the build step, because it's kind of default logic.

Note the documentation for OneTimeSetUpAttribute:

Summary: Identifies a method that is called once to perform setup before any child tests are run.

Although it should work for parallel test runs for a single project, there could be obvious troubles when running two test projects simultaneously, since the config would get overwritten.

However, it is still suitable for containerized test runs, like in Travis.

Maxa answered 12/12, 2018 at 14:20 Comment(0)
A
4

Mercifully there is now a way to set the name of the expected configuration file at runtime. You can set the APP_CONFIG_FILE data for the current app domain.

I created the following SetUpFixture to do this automatically:

[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);
    }
}

The relevant GitHub discussions are:

Apteral answered 28/10, 2021 at 1:2 Comment(0)
E
2

The ConfigurationManager API will only use the configuration of the app that is currently running. In a unit test project, this means the app.config of the test project, not the console application.

.NET Core Applications aren't supposed to use app.config or ConfigurationManager, as it is a legacy "full framework" configuration system.

Consider using Microsoft.Extensions.Configuration instead to read JSON, XML or INI configuration files. See this doc: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration

Essa answered 7/2, 2018 at 15:10 Comment(11)
I'm a little confused as to what your first paragraph is implying.. The unit test project does have it's own app.config.Subjoinder
Then add one and see what happens.Essa
Add what..? I don't see what there is to add. I said it does already have its own app.config. Both projects do.Subjoinder
The test project already has one? I was implying that you add an app.config to the unit test project and see if the AppSettings will show up.Essa
#17580985Orgell
Yes, the test project has one. I'll post a screenshot of the solution explorer.Subjoinder
You are doing a unit test and in unit test your concentration should be the particular method trying to test and should remove extraneous dependencies. in this case, try mocking/moleing(use Microsoft Mole and Pex or moc) system.configuration classOrgell
@MartinUllrich - So if you create a new .net core 2.0 unit test project and use the config I've posted about, does ConfigurationManager.AppSettings[index] fail for you as well?Subjoinder
wait is the full framework test a "classic" test project? or a new sdk-style test project? The classic one might work, the new sdk-style tests won't have this afaik since the config would apply to only the host, which would be a test runner executable.Essa
did you ever figure out a solution to this? Seems silly that ConfigurationManager work in framework, standard, and core, except for in unit testsMaria
It is just a naive implementation reading directly from xml at the moment. bottom line: build proper abstractions and use (manual or automatic) DI so you're not dependent on a runtime feature and the code becomes testableEssa
I
0

Usually in .NET Framework projects, any App.config file was copied to the bin folder by Visual Studio, with the name of your executable (myApp.exe.config) so it could be reachable in runtime. Not anymore in .NET Standard or Core Framework. You must manually copy and set the file in the bin/debug or release folder. After that it could be get with something like:

                string AssemblyName = System.IO.Path.GetFileName(System.Reflection.Assembly.GetEntryAssembly().GetName().CodeBase);
            AppConfig = (System.Configuration.Configuration)System.Configuration.ConfigurationManager.OpenExeConfiguration(AssemblyName);
Incandesce answered 27/4, 2018 at 21:30 Comment(0)
S
0

While app.config exists in the root project folder add below string to Post-build event command line

xcopy /Y $(ProjectDir)app.config $(ProjectDir)$(OutDir)testhost.dll.config*
Sassafras answered 26/1, 2022 at 15:2 Comment(2)
There are twelve existing answers to this question, including a top-voted, accepted answer with forty six votes. Are you certain your solution hasn't already been given? If not, why do you believe your approach improves upon the existing proposals, which have been validated by the community? Offering an explanation is always useful on Stack Overflow, but it's especially important where the question has been resolved to the satisfaction of both the OP and the community. Help readers out by explaining what your answer does different and when it might be preferred.Compagnie
sure, I missed the answer given by Jehedi Zafoom in this thread, his solution is very close to mine. While selected answer requires creating and editing of msbild file, there is a dedicated Post-build event tool that is targeted for resolving of post-build issues. This approach is essential in large solutions , when each project uses the same app.config file which is located in dedicated folder.Sassafras
G
-1

Add the configuration file

First, add a appconfig.json file to the Integration test project

Configure the appconfig.json file to be copied to the output directory by updating

enter image description here

Add NuGet package

  • Microsoft.Extensions.Configuration.Json

Use the configuration in your unit tests

[TestClass]
public class IntegrationTests
{


    public IntegrationTests()
    {
        var config = new ConfigurationBuilder().AddJsonFile("appconfig.json").Build();

        _numberOfPumps = Convert.ToInt32(config["NumberOfPumps"]);

        _numberOfMessages = Convert.ToInt32(config["NumberOfMessages"]);

        _databaseUrl = config["DatabaseUrlAddress"];
    }
} 
Gullett answered 30/12, 2019 at 16:34 Comment(1)
AddJsonFile not exist in .net core 3.1Engineer

© 2022 - 2024 — McMap. All rights reserved.