Introduction
Let me just make things clear. This post is not about creating mocks.
This post is about really unit testing the different scenarios you have when working with configuration files:
- What if the section is missing?
- What if required fields are missing?
- What values can I expect when optional fields are missing?
- ...
To get started, you need the following:
NUnit is the testing framework we'll use to perform our unit tests.
Ninject will be for dependency injection.
TestDriven.NET will be used to run the tests.
And I presume you have basic knowledge of unit testing and dependency injection.
Preparing the Application
Ok, first, we'll create a command line application that reads the App.config and displays the contents.
In brief, this is how the application will work:
- There will be a
ConfigurationSection
mapping the fields with the App.config
(ServerConfigurationSection
) - This section implements the
IServerConfiguration
interface - Then we'll have the
ServerConfigurationProvider
, this class is responsible for reading the configuration file and throwing exceptions if something goes wrong. - The provider implements
IServerConfigurationProvider
that will be registered in our DI container.
Creating the ServerConfigurationSection
So first, we'll create the ServerConfigurationSection
responsible for mapping the fields:
public class ServerConfigurationSection : ConfigurationSection, IServerConfiguration
{
[ConfigurationProperty("server", IsRequired = true)]
public string Server
{
get { return (string)this["server"]; }
set { this["server"] = value; }
}
[ConfigurationProperty("port", IsRequired = false)]
public int Port
{
get { return (int)this["port"]; }
set { this["port"] = value; }
}
}
public interface IServerConfiguration
{
int Port { get; set; }
string Server { get; set; }
}
As you can see we have 2 fields, Server
and Port
. Port is an optional field.
Now that we have this section, we can create the following App.config
file:
="1.0"="utf-8"
<configuration>
<configSections>
<section name="srvConfig"
type="Sandrino.MyApplication.ServerConfigurationSection, Sandrino.MyApplication"/>
</configSections>
<srvConfig server="sandrino.loc" port="1986"/>
</configuration>
Creating the ServerConfigurationProvider
Now that we have a ConfigurationSection
, we can go ahead and create the provider.
Our provider will be read the App.config using the mapping available in the configuration section.
This is the base class that can be reused to quickly create new providers:
public abstract class ConfigurationProviderBase<TSection, TException>
where TSection : ConfigurationSection
where TException : Exception, new()
{
private string sectionName;
private System.Configuration.Configuration config;
public ConfigurationProviderBase(string sectionName)
{
this.sectionName = sectionName;
}
public void SetConfigurationFile(string file)
{
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
fileMap.ExeConfigFilename = file;
config = ConfigurationManager.OpenMappedExeConfiguration
(fileMap, ConfigurationUserLevel.None);
}
protected TSection Read()
{
TSection section = GetSection() as TSection;
if (section == null)
throw new TException();
return section;
}
private object GetSection()
{
if (config != null)
return config.GetSection(sectionName);
else
return ConfigurationManager.GetSection(sectionName);
}
}
What this class does in a few words:
- It accepts a section name to read. This is how you'll call the
ConfigurationSection
in your App.config.
In our example, this is srvConfig
. - Accepts a custom configuration file (using
SetConfigurationFile
) - Can read a configuration section from the App.config or will throw an exception if the section is not found
Now to use it for our ServerConfigurationSection
, we'll create the ServerConfigurationProvider
:
public class ServerConfigurationProvider :
ConfigurationProviderBase<ServerConfigurationSection,
ServerConfigurationMissingException>, IServerConfigurationProvider
{
public ServerConfigurationProvider()
: base("srvConfig")
{
}
public new IServerConfiguration Read()
{
return base.Read();
}
}
public interface IServerConfigurationProvider
{
IServerConfiguration Read();
}
As you can see, we just inherit from the ConfigurationProviderBase
and just return the interface IServerConfiguration
instead of the ServerConfigurationSection
.
Why? I'm not going to go into "What is a loosly coupled architecture", but if we do it this way the reference to System.Configuration
(and the assembly) stays minimal.
Creating the Application
First we'll need to make sure our application knows that IServerConfigurationProvider
== ServerConfigurationProvider
.
Later on, we'll see why we also abstract the provider to an interface.
First, we'll use Ninject to bind the provider to its interface. Any other dependency injector will also do.
public class ConfigurationModule : StandardModule
{
public override void Load()
{
Bind<IServerConfigurationProvider>().To<ServerConfigurationProvider>();
}
}
Finally, we can go ahead and create our command line application:
class Program
{
static void Main(string[] args)
{
using (IKernel krn = new StandardKernel())
{
krn.Load(new ConfigurationModule());
var prov = krn.Get<IServerConfigurationProvider>();
var config = prov.Read();
Console.WriteLine("Server: {0}", config.Server);
Console.WriteLine("Port: {0}", config.Port);
Console.Read();
}
}
}
Again, if you can't read the comments:
- First we create a new kernel (or you might call it a container) and load the
ConfigurationModule
. This will create the bindings for the provider. - Then using
Get
on the kernel, we'll get an instance of the provider and we read the configuration file. - And finally we display the contents.
And shazooom... it works.
Testing the Configuration
This is where it gets interesting. Now we'll need to test the possible outcomes when reading the configuration file.
- What if the section is missing?
- What if required fields are missing?
- What values can I expect when optional fields are missing?
- ...
Setting up the Test Project
First of all, we'll need to create a new project of type class library. We also add a reference to Ninject, NUnit, our main project.
And finally, we also create multiple configuration files. Each configuration file will have something different (a value missing, the section missing, ...) to cover each possible scenario.
Now, before writing the actual tests, remember I said that we'll see later why ServerConfigurationProvider
was also abstracted to the interface IServerConfigurationProvider
?
Because, using Ninject we can do some pretty cool stuff. Look at this:
private void RegisterConfiguration(string filename)
{
krn = new StandardKernel(new InlineModule(m => m
.Bind<IServerConfigurationProvider>()
.ToMethod<IServerConfigurationProvider>(
(e) =>
{
ServerConfigurationProvider config = new ServerConfigurationProvider();
config.SetConfigurationFile(filename);
return config;
})
)
);
}
In English, this would be:
- Bind the interface
IServerConfigurationProvider
to a method - This method should return an
IServerConfigurationProvider
- We declare the method directly (inline) using a lambda expression
And in the method we declare, we just create a new configuration provider and pass it our custom filename.
But the actual tests and event the classes that could use the provider are not aware of this.
The great thing is, maybe next year you'll move to a registry based configuration.
You'll just need to create 2 new classes (maybe ServerRegistryProvider
and ServerRegistryConfiguration
) implementing the correct interfaces.
After that, you'll just need to update the bindings and your whole application stays untouched.
Writing a Test
First, we have the configuration file:
="1.0"="utf-8"
<configuration>
<configSections>
<section name="srvConfig"
type="Sandrino.MyApplication.ServerConfigurationSection, Sandrino.MyApplication"/>
</configSections>
<srvConfig port="1986"/>
</configuration>
As you can see, the server field is missing. But this is a required field! Ooh-ooh!
Let's create a test for this scenario:
[Test]
[ExpectedException(typeof(ConfigurationErrorsException))]
public void ServerMissing()
{
RegisterConfiguration("Configurations\\ServerMissing.config");
var prov = krn.Get<IServerConfigurationProvider>();
var section = prov.Read();
Assert.IsNull(section.Server);
}
And there you go. We register our provider to use ServerMissing.config
. After that, we'll try to read the configuration file and it will raise an exception!
Finally, you can run the test by right clicking on a method:
Take a look in the downloadable solution. It contains a few tests for different scenarios.
I hope you enjoyed the article!