FunLovinCoder
FunLovinCoder

Reputation: 7877

Manipulating the app.config file for unit tests

I have isolated the NUnit tests for my C# app in an assembly called Tests.dll. The associated configuration file is called Tests.dll.config. This is what Nunit uses rather than my app's actual config file. It looks like this (only showing a couple of config options there are lots more):

<?xml version="1.0" encoding="utf-8"?>

<configuration>
  <appSettings>
    <add key="useHostsFile" value="true" />
    <add key="importFile" value="true" />

   </appSettings>
</configuration>

To ensure my app is thoroughly tested, I will need to change config options between tests. After I have run a couple of tests, I would like to add some new config values to the file and have these used by subsequent tests. What code would I need to add do this?

Upvotes: 9

Views: 9375

Answers (4)

David Burg
David Burg

Reputation: 1163

I had a case where my config readers were implemented using Lazy singleton pattern to read only once. Such that changing the app.config was not sufficient as the value was already read from the original configuration file.

The singleton does not cross appdomain boundaries however, and you can specify the app.config for new app domains that you create. So I was able to test the Lazy singleton app settings with:

        var otherAppDomainSetup = new AppDomainSetup
        {
            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
            DisallowBindingRedirects = false,
            DisallowCodeDownload = true,
            ConfigurationFile = Path.Combine(AppContext.BaseDirectory, "Artifacts\\Configs\\OtherApp.config")
        };

        var otherAppDomain = AppDomain.CreateDomain(friendlyName: "Other", securityInfo: null, info: otherAppDomainSetup);

        otherAppDomain.DoCallBack(new CrossAppDomainDelegate(() => Assert.AreEqual(expected: ***, actual: ***static singleton call***)));
        AppDomain.Unload(otherAppDomain);

For non-static calls see the example of using CreateInstanceAndUnwrap at https://learn.microsoft.com/en-us/dotnet/api/system.appdomain?view=netframework-4.7.2

Upvotes: 0

Lars Fastrup
Lars Fastrup

Reputation: 5506

Here is my two cents to this challenge. Simply, create a new class AppSettings as an abstraction layer. Under normal operations it will simply read the settings from the application configuration file. But unit tests can override the settings on a per thread basis allowing for unit tests to execute in parallel with different settings.

internal sealed class AppSettings
{
    private static readonly AppSettings instance;
    private static ConcurrentDictionary<int, AppSettings> threadInstances;
    private string _setting1;
    private string _setting2;

    static AppSettings() { instance = new AppSettings(); }

    internal AppSettings(string setting1 = null, string setting2 = null) {
        _setting1 = setting1 != null ? setting1 : Properties.Settings.Default.Setting1;
        _setting2 = setting2 != null ? setting2 : Properties.Settings.Default.Setting2;
    }

    internal static AppSettings Instance {
        get {
            if (threadInstances != null) {
                AppSettings threadInstance;
                if (threadedInstances.TryGetValue(Thread.CurrentThread.ManagedThreadId, out threadInstance)) {
                    return threadInstance;
                }
            }
            return instance;
        }

        set {
            if (threadInstances == null) {
                lock (instance) {
                    if (threadInstances == null) {
                        int numProcs = Environment.ProcessorCount;
                        int concurrencyLevel = numProcs * 2;
                        threadInstances = new ConcurrentDictionary<int, AppSettings>(concurrencyLevel, 5);
                    }
                }
            }

            if (value != null) {
                threadInstances.AddOrUpdate(Thread.CurrentThread.ManagedThreadId, value, (key, oldValue) => value);
            } else {
                AppSettings threadInstance;
                threadInstances.TryRemove(Thread.CurrentThread.ManagedThreadId, out threadInstance);
            }
        }
    }

    internal static string Setting1 => Instance._setting1;

    internal static string Setting2 => Instance._setting2;
}

In the application code, access settings using the static properties:

function void MyApplicationMethod() {
    string setting1 = AppSettings.Setting1;
    string setting2 = AppSettings.Setting2;
}

In unit tests, optionally override selected settings:

[TestClass]
public class MyUnitTest
{
    [TestCleanup]
    public void CleanupTest()
    {
        //
        // Clear any app settings that were applied for the current test runner thread.
        //
        AppSettings.Instance = null;
    }

    [TestMethod]
    public void MyUnitMethod()
    {
        AppSettings.Instance = new AppSettings(setting1: "New settings value for current thread");
        // Your test code goes here
    }
}

NOTE: As all methods of the AppSettings class are declared as internal it is necessary to make them visible to the unit test assembly using the attribute: [assembly: InternalsVisibleTo("<assembly name>, PublicKey=<public key>")]

Upvotes: 1

Kiquenet
Kiquenet

Reputation: 14996

I use this code:

 [TestMethod]
    public void Test_general()
    {
        var cs = new ConnectionStringSettings();
        cs.Name = "ConnectionStrings.Oracle";
        cs.ConnectionString = "DATA SOURCE=xxx;PASSWORD=xxx;PERSIST SECURITY INFO=True;USER ID=xxx";
        cs.ProviderName = "Oracle.DataAccess.Client";

        var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        //config.ConnectionStrings.ConnectionStrings.Clear();
        config.ConnectionStrings.ConnectionStrings.Remove(cs.Name);
        config.ConnectionStrings.ConnectionStrings.Add(cs);
        config.Save(ConfigurationSaveMode.Modified);
        ConfigurationManager.RefreshSection("connectionStrings");

        // your code for your test here
   }

Upvotes: 3

NickD
NickD

Reputation: 2646

I recommend to implement a interface IConfig with properties useHostsFile and importFile. Then i would remove all direct dependecies to this file except in the Class ConfigDefault which implements IConfig. In this implementation you load your normal config file. For each test you can implement another Class which also inherits from IConfig. I suggest to use a Dependecy Injection. Ninject is free and easy to use.

Upvotes: 4

Related Questions