次の方法で共有


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.

Comments

  • Anonymous
    July 11, 2007
    This is an excellent article. Thx!

  • Anonymous
    July 12, 2007
    I would suggest keeping all the configuration file code out of your logic code. The best way I know to do that is with dependency injection (you can find a bunch of articles on the topic here: http://udidahan.weblogs.us/category/dependency-injection/ ) The result is having testable code in which you can easily switch configuration schemes.

  • Anonymous
    July 12, 2007
    Hi Udi As always an excellent observation :) I agree that most of the time, it's probably better to just model your entire API without specifically partitioning off certain portions of it and calling it 'configuration'. That's also the approach taken by WCF, where a Binding isn't the same thing as a BindingElement, although they do share a lot of common structure. This article might have benefited from a disclaimer stating that you should still strive to model your API independently of any particular configuration schema. That's still kind of what's going on in the sample code, but to keep samples simple, it's always necessary to cut a few corners here and there :)

  • Anonymous
    July 28, 2007
    In my former post on Ambient Contexts , I described how you can use CallContext or Thread Local Storage

  • Anonymous
    July 28, 2007
    In my former post on Ambient Contexts , I described how you can use CallContext or Thread Local Storage

  • Anonymous
    July 29, 2007
    I agree with Udi that you should keep configuration file logic out of your logic code.  On the other hand, there are several examples of not doing this in the .NET framework.  For example, SmtpClient works much the same way as Mark's Trajectory class and MembershipProvider only works with a configuration file (unfortunately).

  • Anonymous
    August 05, 2007
    In my opinion, the most important thing to do is to enable a certain degree of flexibility. Is it important to allow developers to use your API without the presence of a configuration file? Yes, for the reasons stated in my post. Is it important to enable developers to use different configuration schemas? Not quite as much, I should think, although YMMV. The purpose of a library is to encapsulate some piece of repeatable work while still enabling flexibility. Creating a configuration schema is, in my opinion, part of that task. As I previously stated, I can think of several examples where you'd want not to use a configuration file. On the other hand, when you want to use a configuration file, in most cases you'd prefer to use a configuration schema that's already defined for you, since that would increase your productivity. The alternative is that you'd have to define your own schema, including deserialization code etc. each time you want to use the library in a configurable manner. There may be cases where that makes sense, but I'd maintain that this is not a mainstream scenario. In any case, when imperative configuration is enabled, you can still implement your own configuration logic if you don't like the default implementation supplied with the library.