I'm working on a project were I'm trying to port several libraries from .NET Framework 4.5.2 to .NET Core 2, and I'm facing some problems trying to read legacy app.config appsettings in unit tests. To reduce the problem to a bare minimum reproduction scenario I've created the following project in VS2017:
I have the app.config file:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="TestKey" value="20" />
</appSettings>
<configSections>
</configSections>
</configuration>
And the UnitTest1.cs file:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Configuration;
namespace SimpleTestsUnits
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void FromConfigurationManager()
{
Assert.AreEqual("20", ConfigurationManager.AppSettings["TestKey"]);
}
}
}
And upon building this project the SimpleTestsUnits.dll is generated and the SimpleTestsUnits.dll.config is created with the content of the app.config file in the same folder of the SimpleTestsUnits.dll file.
So, when I run the unit test using VS2017 the value of "TestKey" is always null and if I debug into the ConfigurationManager.AppSettings there is no key loaded there.
Exception thrown: 'Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException' in Microsoft.VisualStudio.TestPlatform.TestFramework.dll An exception of type 'Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException' occurred in Microsoft.VisualStudio.TestPlatform.TestFramework.dll but was not handled in user code Assert.AreEqual failed. Expected:<20>. Actual:<(null)>.
What am I missing here? Shouldn't this be working?
MSTest is running as "testhost.dll", which means that ConfigurationManager is reading settings from "testhost.dll.config" when executing under .NET core. It will look for "testhost.dll.config" where the "testhost.dll" is located but it will also also look for "testhost.dll.config" in the location where you have your test dlls.
So copying or renaming your config file in explorer to "testhost.dll.config" will solve the problem.
You can easily automate this by adding the following MSBuild step to the end of the MSTest .csproj file, within the "Project" tag.
<Target Name="CopyAppConfig" AfterTargets="Build" DependsOnTargets="Build">
<CreateItem Include="$(OutputPath)$(AssemblyName).dll.config">
<Output TaskParameter="Include" ItemName="FilesToCopy"/>
</CreateItem>
<Copy SourceFiles="@(FilesToCopy)" DestinationFiles="$(OutputPath)testhost.dll.config" />
</Target>
Source: ( https://github.com/Microsoft/testfx/issues/348#issuecomment-454347131 )
When you execute the tests, the entry assembly is not an assembly with your tests. You can check it by adding following line to your test and debugging it:
var configLocation = Assembly.GetEntryAssembly().Location;
In my case configLocation
was c:\\Users\\myusername\\.nuget\\packages\\microsoft.testplatform.testhost\\15.3.0-preview-20170628-02\\lib\\netstandard1.5\\testhost.dll
So ConfigurationManager
expects to find app.config
at testhost.dll.config
in specified directory. I've copied it to this location and the test passed ok (after slight modification of the config, see below).
Another problem is that your app.config is not fully correct. configSections
element should be the first in <configuration>
root. So just remove configSections
element as it's empty or adjust your app.config
in the following way:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
</configSections>
<appSettings>
<add key="TestKey" value="20" />
</appSettings>
</configuration>
Of course, it's a bad way to place config file near testhost.dll
. You could change the path from which ConfigurationManager
loads application config with ConfigurationManager.OpenExeConfiguration
call:
[TestMethod]
public void UnitTest1()
{
// Put your Test assembly name here
Configuration configuration = ConfigurationManager.OpenExeConfiguration(@"SimpleTestsUnits.dll");
Assert.AreEqual("20", configuration.AppSettings.Settings["TestKey"].Value);
}
But unfortunately this approach requires modification of your code under test.
You can try this class for size.
public static class AppConfig
{
const string Filename = "app.config";
static readonly Configuration Configuration;
static AppConfig()
{
try
{
Configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
if (!Configuration.HasFile)
throw new ConfigurationErrorsException();
}
catch
{
try
{
var configmap = new ExeConfigurationFileMap
{
ExeConfigFilename = Filename
};
Configuration = ConfigurationManager.OpenMappedExeConfiguration(configmap, ConfigurationUserLevel.None);
}
catch
{
}
}
}
public static string Get(string key, string @default = "")
{
return Configuration?.AppSettings?.Settings[key]?.Value ?? @default;
}
}
I implemented the solution that Oskar Sjöberg presented. It worked well for our solutions until we began to use multi-targeting.
The MSBuild step that Oskar presented would attempt to copy files from a location that was not being used as an output directory for building. It would produce an error as it could not find the app.config
files that it was looking for.
I ended up adding Condition="'$(OutDir)' != ''"
to the target definition.
Note: I have not tested this across many platforms or many project styles. It may be the case that this prevents the copying from taking place.
It then looked like this:
<Target Name="CopyAppConfig" AfterTargets="Build" DependsOnTargets="Build" Condition="'$(OutDir)' != ''">
<CreateItem Include="$(OutputPath)$(AssemblyName).dll.config">
<Output TaskParameter="Include" ItemName="FilesToCopy" />
</CreateItem>
<Copy SourceFiles="@(FilesToCopy)" DestinationFiles="$(OutputPath)testhost.dll.config" />
</Target>
BTW I wanted to comment on that post but I do not have enough reputation yet.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.