The problem
Usually, you will use your app.config / web.config file to store some configuration. And so you will have some code depending on it. Let's take for instance :
public class MyService
{
public void DoSomething()
{
string setting = ConfigurationManager.AppSettings["SomeSetting"];
if ( setting == null )
throw new ConfigurationErrorsException("Your settings shall be present");
Console.WriteLine(setting);
}
}
and of course, you will test your code. So you will have a test DLL, that will hold both a config file, and a test.
<configuration>
<appSettings>
<add key="SomeSetting" value="My Setting Value" />
</appSettings>
</configuration>
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
var service = new MyService();
service.DoSomething();
}
}
When you will write a more complete test, you will probably start using a mock framework. Personnaly my favorite ones are Rhino Mock from Ayende, and Moles & Stubs that is developped by Microsoft Research. (Note that Moles & Stubs is getting replaced by Fakes under Visual Studio 2001).
Both framework has its own benefits and disadvantage, but this is not the place for a debate. Personally, in the late years, my preference has gone to Moles & Stubs, for its power and simplicity. So let's say you are using Moles for your test :
(Note that the goal here is not to show the use of Moles. So we just assumere you are using some moles in your test, and so applying the HostType attribute).
[TestMethod]
[HostType("Moles")]
public void TestMethod1()
{
var service = new MyService();
service.DoSomething();
}
Run your test again and that time it crashes : your configuration file is not found anymore.
What happens ?
If you have already executed code from an external exe via .NET code, you may have already coped with the problem : your code is becoming the main AppDomain, and thus the searched config file correspond to your own exe and no longer to the one of the external exe. I can imagine the problem is similar here.
Note that this bug exist only when you use the HostType("Moles") and not when you only use stubs.
What do we want ?
Let's find a workaround to that bug. Moles & Stubs allows you to mock any method of the .NET framework. So we can mock also the ConfigurationManager ?
Here is the idea, using moles to "mole" the ConfigurationManager in order to let him "eat" the correct configuration file.
Of course, we need to do that in each test that is running with the HostType("Moles"). So the best would be to have that code in a global location that could be applied to every single corresponding method. This is typically where we need some AOP (Aspect Oriented Programming).
The solution - Step 1
There is many AOP framework available for .NET. As far as I am concerned, I think the best one (simpler to use and more powerful) is PostSharp.
So we want to have an attribute that can be applied to every method having the HostType attribute.
So we start by referencing PostSharp using NuGet, and we declare an attribute that will target the corresponding Test methods.
[Serializable]
[MulticastAttributeUsage(MulticastTargets.Method, AllowMultiple = false)]
public class ReconcileConfigurationManagerAttribute : OnMethodBoundaryAspect
{
/// <summary>
/// Returns whether the currently under investigation method shall we woven or not.
/// We are searching any method that has the attributes [TestMethod] and [HostType("Moles")]
/// </summary>
/// <param name="method">The method being investigated</param>
/// <returns>True if the method shall be woven, otherwise false</returns>
public override bool CompileTimeValidate(MethodBase method)
{
if ( method.GetCustomAttributes(typeof(TestMethodAttribute), false).Length == 0 )
return false;
var hostType = method.GetCustomAttributes(typeof(HostTypeAttribute), false)
.Cast<HostTypeAttribute>()
.FirstOrDefault();
if ( hostType == null || hostType.HostType != "Moles" )
return false;
return true;
}
}
Step 2 : let's reconcile the ConfigurationManager
Now we need to update the attribute so we can use Moles to let the ConfigurationManager work again. To do that, we need to override the OnEntry method of our attribute.
/// <summary>
/// When entrying the method, let's mole the ConfigurationManager, so he uses the correct config file.
/// </summary>
/// <param name="args">The execution argument</param>
public sealed override void OnEntry(MethodExecutionArgs args)
{
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
fileMap.ExeConfigFilename = args.Method.DeclaringType.Assembly.GetName().Name + ".dll.config";
var config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
MConfigurationManager.GetSectionString = (sectionName) =>
{
//Note that when your code is using AppSettings, you do not work with the AppSettingsSection
//but with a NameValueCollection.
//So we need to handle that differently
var section = config.GetSection(sectionName);
if ( section is AppSettingsSection )
{
var collection = new NameValueCollection();
foreach ( KeyValueConfigurationElement item in ( (AppSettingsSection)section ).Settings )
collection.Add(item.Key, item.Value);
return collection;
}
return section;
};
base.OnEntry(args);
}
What we do here is quite simple : the application should have used the app.config file that you have defined in your test assembly. So we are just taking the name of the test assembly, and concatening ".dll.config" : this correspond to the name of the config file that should have been used. And so, when our production code is calling the method ConfigurationManager.GetSection, we are just "redirecting" to the correct file.
Note that calling the GetSection method returns you a class inheriting from ConfigurationSection. However, we need to do a special treatment for the section appSettings. Indeed when you call ConfigurationManager.AppSettings, you receive a NameValueCollection and not the AppSettingsSection.
Step 3 : let's use our attribute
So now that our attribute is ready for use, we just need to use it in our test library. That's so simple : let's edit our AssemblyInfo.cs to apply our attribute to the whole assembly.
[assembly: ReconcileConfigurationManager]
//This line may be needed
//[assembly: MoledType(typeof(ConfigurationManager))]
PostSharp will do it's job and apply our code to any TestMethod that is using Moles. And, you have simply worked around the bug !
Note that for performance reasons, Moles & Stubs does not allow to mole some methods from the code of the .NET framework. To do this, you need to explicitely allow him to mole those types. That's the goal of the MoledTypeAttribute. In some of my tests, this attribute was not needed.
Limitations
Does this solution is a complete workaround for that bug ? I need to add some more stuff.
PostSharp is partly commercial
We are relying here on PostSharp which is a commercial product (even though it has a free licence that allows you to do a lot of great stuffs). The ability of declaring the attribute on the assembly and letting him to "propagate" to any method of that assembly is called multicast. This capability is available only with the commercial version.
If you do not want to invest (personal advice, I do think that this product worth 10x its price, and I recommend you to buy it. You won't be able to let it aside !), the described solution can still be used. But you will need to set this attribute individually on each TestMethod for which you need to "correct" the Moles bug.
Moles & Stubs cannot moles static constructor
There is another limitation to Moles & Stubs. You can mole what is called "predictibly". Unfortunately, this is not the case of a static constructor that will get called once you first call a member of a class. So any code that is inside of a static constructor cannot be replaced / detoured. So even if you are using this solution, if one of your static constructor is calling ConfigurationManager, you are stuck.
There is one possibility offered by Moles & Stubs : there is one attribute that is MolesEraseStaticConstructor. This will simply erase the cctor for the type given in the attribute constructor. This may not be sufficient in some of your cases anyway.
Conclusion
You were in love with Moles & Stubs, but quite bothered by this annoying bug ? Don't be anymore ! It can worked around, simply, and quite transparently.