Reputation: 1629
I have a class with a static constructor which I use to read the app.config values. How do I unit test the class with different configuration values. I'm thinking of running each test in different app domain so I can have static constructor executed for each test - but I have two problems here:
1. I do not know how to run each test run in separate app domain and
2. how do I change configuration settings at run time?
Can someone please help me with this? Or anyone has a better solution? Thanks.
Upvotes: 5
Views: 3455
Reputation: 3271
I had a similar problem in that I wanted to check how my application works with different or missing values held in app.config. Much searching of the Internet and of Stckoverflow led me to create this:
Sample of a centralised Application Settings where I keep/access settings. Also the static class I want to test with various settings read from my app.config file.
public class ApplicationSettings
{
static ApplicationSettings()
{
ApplicationName = Settings.Default.ApplicationName;
ConnectionString = GetConnectionString("TestingDbConnection");
}
public static String ApplicationName { get; }
public static String ConnectionString { get; }
/// <summary>
/// Gets the connection string.
/// </summary>
/// <param name="dataConnectionName">Name of the data connection.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">dataConnectionName</exception>
public static String GetConnectionString(String dataConnectionName)
{
ConnectionStringSettings connectionStringSettings = ConfigurationManager.ConnectionStrings[dataConnectionName];
if (connectionStringSettings == null)
{
String errorMessage = $"Cannot load Connection named '{dataConnectionName}'. Check to make sure the connection is defined in the Configuration File.";
throw new ArgumentNullException(nameof(dataConnectionName), errorMessage);
}
String retVal = connectionStringSettings.ConnectionString;
return retVal;
}
}
In order to switch out the app.config files during Unit Tests, I created this class AppConfigModifier
/// <summary>
/// The App Config Modifier class
/// </summary>
public class AppConfigModifier : IDisposable
{
/// <summary>
/// Initialises a new instance of the <see cref="AppConfigModifier"/> class.
/// </summary>
/// <param name="appConfigFile">The application configuration file.</param>
public AppConfigModifier(String appConfigFile)
{
OldConfig = AppDomain.CurrentDomain.GetData("APP_CONFIG_FILE").ToString();
TargetDomain = AppDomain.CreateDomain("UnitTesting", null, AppDomain.CurrentDomain.SetupInformation);
FileInfo fi = new FileInfo(OldConfig);
String newConfig = Path.Combine(fi.DirectoryName, appConfigFile);
TargetDomain.SetData("APP_CONFIG_FILE", newConfig);
ResetConfigMechanism();
}
/// <summary>
/// Gets or sets a value indicating whether [disposed value].
/// </summary>
/// <value>
/// <c>true</c> if [disposed value]; otherwise, <c>false</c>.
/// </value>
private Boolean DisposedValue { get; set; }
/// <summary>
/// Gets or sets the old configuration.
/// </summary>
/// <value>
/// The old configuration.
/// </value>
private String OldConfig { get; }
/// <summary>
/// Gets the target domain.
/// </summary>
/// <value>
/// The target domain.
/// </value>
public AppDomain TargetDomain { get; }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
if (!DisposedValue)
{
TargetDomain.SetData("APP_CONFIG_FILE", OldConfig);
ResetConfigMechanism();
AppDomain.Unload(TargetDomain);
DisposedValue = true;
}
GC.SuppressFinalize(this);
}
/// <summary>
/// Resets the configuration mechanism.
/// </summary>
private static void ResetConfigMechanism()
{
Type configurationManagerType = typeof(ConfigurationManager);
FieldInfo initStateFieldInfo = configurationManagerType.GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static);
if (initStateFieldInfo != null)
{
initStateFieldInfo.SetValue(null, 0);
}
FieldInfo configSystemFieldInfo = configurationManagerType.GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static);
if (configSystemFieldInfo != null)
{
configSystemFieldInfo.SetValue(null, null);
}
Assembly currentAssembly = configurationManagerType.Assembly;
Type[] allTypes = currentAssembly.GetTypes();
Type clientConfigPathsType = allTypes.First(x => x.FullName == "System.Configuration.ClientConfigPaths");
FieldInfo currentFieldInfo = clientConfigPathsType.GetField("s_current", BindingFlags.NonPublic | BindingFlags.Static);
if (currentFieldInfo != null)
{
currentFieldInfo.SetValue(null, null);
}
}
}
For my purposes, if the DatabaseConnection is missing, I want it to throw a nice verbose exception (ArgumentNullException is something we think fits the bill). The Test Case below ensures this is what happens:
[TestClass]
[DeploymentItem("NoConnectionStringConfig.App.config")]
public class AppConfigSwitchTests
{
[TestMethod]
public void Test_MissingDatabaseConnection()
{
using (AppConfigModifier newDomain = new AppConfigModifier("NoConnectionStringConfig.App.config"))
{
String connectionName = "TestingDbConnection";
String parameterName = "dataConnectionName";
String expectedMessage1 = $"Cannot load Connection named '{connectionName}'. Check to make sure the connection is defined in the Configuration File.\r\nParameter name: {parameterName}";
Exception actualException = null;
try
{
String assemblyName = typeof(ApplicationSettings).Assembly.GetName().Name;
String typeName = typeof(ApplicationSettings).FullName;
_ = newDomain.TargetDomain.CreateInstanceAndUnwrap(assemblyName, typeName);
ApplicationSettings.GetConnectionString(connectionName);
}
catch (Exception exception)
{
actualException = exception;
}
Assert.IsNotNull(actualException);
Assert.IsInstanceOfType(actualException, typeof(TargetInvocationException));
Exception innerException = actualException.InnerException;
Assert.IsInstanceOfType(innerException, typeof(TypeInitializationException));
Exception raisedException = innerException.InnerException;
Assert.IsInstanceOfType(raisedException, typeof(ArgumentNullException));
ArgumentNullException anException = raisedException as ArgumentNullException;
String actualMessage1 = anException.Message;
Assert.AreEqual(expectedMessage1, actualMessage1);
String actualParameterName = anException.ParamName;
Assert.AreEqual(parameterName, actualParameterName);
}
}
}
And, the final piece is the content for the app.config file that is loaded in place, conveniently called NoConnectionStringConfig.App.config.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
</startup>
<connectionStrings>
<clear/>
<add name="UnitTesting" providerName="System.Data.SqlClient" connectionString="Server=TheServer;Database=UnitTesting;User Id=TheUserId;Password=ThePassword;"/>
</connectionStrings>
</configuration>
Upvotes: 0
Reputation: 976
I had the same exact problem recently. The only difference was that the configuration value was coming from database instead of app.config. I was able to resolve it using TypeInitializer.
[Test]
public void TestConfigurationInStaticConstructor()
{
// setup configuraton to test
// ...
// init static constructor
ReaderTypeInit();
// Assert configuration effect
// ...
// reset static ctor to prevent other existing tests (that may depend on original static ctor) fail
ReaderTypeInit();
}
// helper method
private void ReaderTypeInit()
{
typeof(< your class with static ctor>).TypeInitializer.Invoke(null, new object[0]);
}
Upvotes: 0
Reputation: 2047
If you read from (Web)ConfigurationManager.AppSettings
, that is just a NameValueCollection, so you can replace your code that reads ConfigurationManager.AppSettings
directly with code, that reads from any NameValueCollection.
Just move out your actual configuration parsing to a static method from the static ctor. Static ctor calls that static method and passes ConfigurationManager.AppSettings
, but you can call that parser method from the test code, and verify the config parsing without actually touching a file, or messing with appdomains.
But on the long run, really inject your configuration parameters as seldary suggested. Create a configuration class, read the actual values at application start, and set up your IoC container to supply the same configuration instance to all requesters.
This makes further testing easier too, because you classes don't read from a global static configuration instance. You can just pass in a specific configuration instance for differet tests. Of course create a factory method for your tests, to construct a global configuration, so you don't have to do it manually all the time...
Upvotes: 0
Reputation: 6256
You don't need to test .Net being able to load data from config files.
Instead, try to concentrate on testing your own logic.
Change your class so that it gets the configuration values from its constructor (or via properties), and then test it as you would with any other dependency.
Along the way you have also moved your class towards SRP.
As per the configuration loading - concentrate this logic in a separate, non-static class.
EDIT:
Separate the configuration logic into another class. something like this:
public static class ConfigurationLoader
{
static ConfigurationLoader()
{
// Dependency1 = LoadFromConfiguration();
// Dependency2 = LoadFromConfiguration();
}
public static int Dependency1 { get; private set; }
public static string Dependency2 { get; private set; }
}
Then, when you instantiate your class, inject it with the dependencies:
public class MyClass
{
private readonly int m_Dependency1;
private readonly string m_Dependency2;
public MyClass(int dependency1, string dependency2)
{
m_Dependency1 = dependency1;
m_Dependency2 = dependency2;
}
public char MethodUnderTest()
{
if (m_Dependency1 > 42)
{
return m_Dependency2[0];
}
return ' ';
}
}
public class MyClassTests
{
[Fact]
public void MethodUnderTest_dependency1is43AndDependency2isTest_ReturnsT()
{
var underTest = new MyClass(43, "Test");
var result = underTest.MethodUnderTest();
Assert.Equal('T', result);
}
}
...
var myClass = new MyClass(ConfigurationLoader.Dependency1, ConfigurationLoader.Dependency2);
You could go on and use IOC containers, but your problem of testing MyClass with different inputs is solved by this simple testable design.
Upvotes: 0
Reputation: 1668
Personally I would just stick your static constructor into a static method then execute that method in the static block.
Upvotes: 0