Imperative Configuration

How can I unit test my application with different configuration settings? A couple of months back, a reader asked me this question, and while I provided an immediate response via email, I think that the question deserves a post of its own.

In .NET, using XML-based application configuration is easy and built into the framework, so it's common for applications or libraries to depend on a configuration file. The configuration system in .NET is essentially read-only. While you can write to a config file, this will not reload the configuration settings for a running application. To reload the configuration settings, the AppDomain must be restarted.

In unit testing, this is not particularly practical, since a test suite executes all test cases in a single AppDomain. As a consequence, once a configuration section is read from the configuration file, it cannot be changed by changing the declarative (that is: XML) configuration. This is a challenge if you need to test your code with different, mutually exclusive configuration settings.

This is another good reason for providing an imperative way to define configuration settings for your code. Essentially, file-based configuration should be treated as a fall-back mechanism that only applies if the configuration isn't supplied imperatively.

As a simplistic example, imagine that you are developing a library of Newtonian mechanics to calculate projectile trajectories. Your first take on the Trajectory class looks something like this:

 public class Trajectory
{
    private const double gravitationalAcceleration_ = 9.80665;
    private double range_;
 
    public Trajectory(double angleInDegrees,
        double initialVelocity)
    {
        double angleInRadians = angleInDegrees * Math.PI / 180;
        this.range_ =
            (Math.Pow(initialVelocity, 2) * Math.Sin(angleInRadians)) /
            Trajectory.gravitationalAcceleration_;
    }
 
    public double Range
    {
        get { return this.range_; }
    }
}

For simplicity's sake, the Trajectory class only exposes the Range property, which provides the maximum range given the firing angle and initial velocity of the projectile. The formula used in the constructor is the formula for calculating the range, but the details of that are not terribly important besides the point that you need to know the gravitational acceleration. The constant value of 9.80665 is Earth's gravity.

Now, a new requirement occurs: Some customers on Mars want to use your library to calculate trajectories, but obviously while using Mars' gravitational acceleration (which is 3.69). For that reason, you decide to move the gravitational acceleration value to a custom configuration section.

 <configuration>
   <configSections>
     <section name="newtonianConfiguration"
              type="Ploeh.Samples.Newtonian.NewtonianConfigurationSection, Ploeh.Samples.Newtonian" />
   </configSections>
   <newtonianConfiguration gravitationalConstant="9.80665" />
 </configuration>

In this example, the custom configuration section is obviously very simple, but in a more realistic scenario, it would be much more complex. Here, we only have the single gravitationalConstant attribute, which is set to Earth's gravity.

Instead of using a constant, the Trajectory constructor must now read the gravitational acceleration from the application configuration.

 public Trajectory(double angleInDegrees,
     double initialVelocity)
 {
     NewtonianConfigurationSection config =
         ConfigurationManager.GetSection(
         NewtonianConfigurationSection.ConfigurationName)
         as NewtonianConfigurationSection;
     if (config == null)
     {
         throw new InvalidOperationException("No configuration");
     }
  
     double angleInRadians = angleInDegrees * Math.PI / 180;
     this.range_ =
         (Math.Pow(initialVelocity, 2) * Math.Sin(angleInRadians)) /
         config.GravitationalConstant;
 }

The modified constructor now loads the custom configuration section from the configuration file and throws an exception if it's not found. Otherwise, it uses the gravitational acceleration from the configuration section to calculate the range.

This ought to effectively address the need to support other gravities, since the Mars customers can now configure their applications to use Mars' gravitational acceleration. The main problem that's left now is that you can't really unit test the Trajectory class with both Earth's and Mars' gravity.

While you can add an app.config file to your unit test project, you can only add one. Once the configuration is read from the configuration file, it's cached and never refreshed for the lifetime of the AppDomain, so you can't change the configuration from test case to test case.

In the very simple example, one solution could be to create two different unit test projects - one with Earth gravity, and one with Mars gravity, but if you have a more complex configuration schema with more possible permutations, this becomes impractical very fast.

The best solution is to provide an imperative override to the configuration. This is easily done in the example:

 public class Trajectory
 {
     private double range_;
  
     public Trajectory(double angleInDegrees,
         double initialVelocity)
     {
         NewtonianConfigurationSection config =
             ConfigurationManager.GetSection(
             NewtonianConfigurationSection.ConfigurationName)
             as NewtonianConfigurationSection;
         if (config == null)
         {
             throw new InvalidOperationException("No configuration");
         }
  
         this.Initialize(angleInDegrees,
             initialVelocity, config.GravitationalConstant);
     }
  
     public Trajectory(double angleInDegrees,
         double initialVelocity, double gravitationalAcceleration)
     {
         this.Initialize(angleInDegrees, initialVelocity,
             gravitationalAcceleration);
     }
  
     public double Range
     {
         get { return this.range_; }
     }
  
     private void Initialize(double angleInDegrees,
         double initialVelocity, double gravitationalAcceleration)
     {
         double angleInRadians = angleInDegrees * Math.PI / 180;
         this.range_ =
             (Math.Pow(initialVelocity, 2) * Math.Sin(angleInRadians)) /
             gravitationalAcceleration;
     }
 }

The range calculation has been moved to the private Initialize method, and an overloaded constructor that takes the gravitational acceleration as a parameter has been added to the Trajectory class. This makes it very simple to test range calculations for both Earth and Mars in the same test suite:

 [TestMethod]
 public void UseEarthTrajectory()
 {
     Trajectory t = new Trajectory(45, 250, 9.80665);
  
     Assert.AreEqual(4506.5515567659924923447632608029, t.Range, 0.001);
 }
  
 [TestMethod]
 public void UseMarsTrajectory()
 {
     Trajectory t = new Trajectory(45, 250, 3.69);
  
     Assert.AreEqual(11976.740873755886253401835401505, t.Range, 0.001);
 }

In a more realistic scenario with a complex configuration schema, the overloaded constructor should take a parameter representing the entire configuration hierarchy instead of just a value type as the gravitational constant from the example.

Variations where you supply the configuration imperatively by setting a property or calling a method just after you've created your object, but before you begin using it, are valid alternatives in some cases.

As an example, the WCF ServiceHost uses this approach, where you can either use the class without further code, in which case it will use the configuration from the configuration file, or you can add bindings and other configuration settings imperatively (with the AddServiceEndpoint method and other members).

Designing your component with imperative configuration as an option not only lets you unit test different configuration scenarios much easier, but also provides more flexibility for any client consuming your code.