Thursday, September 3, 2009

On abstracting out static contexts

Any test favoring developer will tell you singletons are the bane of testability. Static contexts make testing difficult. When dealing with legacy code, we are often faced with the problem of testing around such problematic coupling. I've had enough opportunity to dable in such code to have developed some strategies in dealing with coupling. In this post I'll present a strategy I refer to as abstracting out singletons.

For the purpose of discussion I present the following code fragment:

public class TightlyCoupled {
 public void quickPurchase(String customerReferenceNumber, 
    int productCode, int quantity) {
  CustomerCatalog customers = CustomerCatalog.getInstance();
  Customer purchaser = customers.find(customerReferenceNumber);
  if (purchaser == null)
   throw new UnknownCustomerException(customerReferenceNumber);
  OrderItem requestedItems = Inventory.getInstance().take(productCode, quantity);
  Order quickOrder = new Order(purchaser);
  quickOrder.add(requestedItems);
  // ...
 }
}

This code is displays the symptoms of coupling that clearly make testing difficult. Can the inventory and customer catalog be easly programmed to behave in a predictable and testable manner? Your answer will be contextual, but often they cannot. In most enterprise application such singletons will bootstrap the initialization of other singletons as well as database connections. Can these services be easily mocked? Doubtful.

I have seen cases where programmers will allow themselves to override the singleton before the test scenario. This works, but does not help to move away from the current programming model.

What we want is to be able to provide an alternate implementation of these services to a constructed instance of the class under test (CUT). The problem with using a dependency injection approach to testing is that most applications developed with singleton spread are rarely blessed with an IoC container to facilitate object construction. Consequently we can rarely allow ourselves to remove the default constructor of the CUT.

So the first thing that I propose is that we consider using a DI approach, and in order to support this we provide two constructors for our CUT: one that allows for the injection of the dependencies, an a default constructor that wires the class with the default dependencies. This quickly leads us to something like this:

public class LessCoupledThroughConstructor {
 private CustomerCatalog customers;
 private Inventory inventory;

 public LessCoupledThroughConstructor(
   CustomerCatalog customers,
   Inventory inventory) {
  this.customers = customers;
  this.inventory = inventory;
 }

 public LessCoupledThroughConstructor() {
  this(CustomerCatalog.getInstance(), Inventory.getInstance());
 }

 public void quickPurchase(
  String customerReferenceNumber, 
  int productCode, int quantity) {
  Customer purchaser = customers.find(customerReferenceNumber);
  if (purchaser == null)
   throw new UnknownCustomerException(customerReferenceNumber);
  OrderItem requestedItems = inventory.take(productCode, quantity);
  Order quickOrder = new Order(purchaser);
  quickOrder.add(requestedItems);
  // ...
 }
}

So now our CUT can be passed a test friendly set of dependencies with a minimal impact on existing code. What's more, if the CUT is normally treated as a singleton also, its default constructor can become private, and its instance accessor would not take any dependencies. The test friendly constructor would remain public.

This approach has one drawback. When working on a legacy system that relied heavily on singleton accessors to initialize its services, I quickly discovered that many of these services had developed a kind of magical initialization order. No one knew for certain what that order was, but many bugs in the system were to it. This minor adjustment I proposed broke the initialization order: the dependency services where initialized in the constructor, and not on first use in the using method. Ka-boom!

No problem. The solution is to introduce a minimal amount of abstraction. I decided to creation some wrapper services. These services delegated operations to the static context on a per-call basis, but where not singletons or static instances themselves. This produces the code bellow:

public class LessCoupledThroughWraping {
 private CustomerCatalog customers;
 private Inventory inventory;
 
 public LessCoupledThroughWraping(
   CustomerCatalogService customers,
   InventoryService inventory) {
  this.customers = customers;
  this.inventory = inventory;
 }

 public LessCoupledThroughWraping() {
  this(new GlobalCustomerCatalogService(), new GlobalInventoryService());
 }

 public void quickPurchase(
   String customerReferenceNumber, 
   int productCode, int quantity) {
  // as previous example
 }
}

To give you an idea of what the wraper services look like:

public class GlobalInventoryService implements InventoryService {
 public OrderItem take(int productCode, int quantity) {
  return Inventory.getInstance().take(productCode, quantity);
 }
}

This approached allows the CUT to define a default set of dependencies, but still provides the possibility to inject dependencies.

These approaches are clearly not good examples of applied TDD, but on large systems that have a great deal of code coupling it is often necessary to introduce such test seams. Gradually a large system becomes split into an IoC container friendly design with well tested components.

Other areas where such a strategies work include all static data, static third party APIs, as well as static platform functinality.

An example that I used recently involves the .NET runtime's “DateTime.Now” which can make testing time dependent behavior unpredictable. My goal was to test that after 10 minutes some condition expired. Obviously I didn't want a test that ran for ten minutes. I abstract out a date and time service and at test time I substitute a date time service on which I can set the desired date and time. This makes testing cleaner and more reliable, and in this last case I only needed to redefine the date/time service's time to jump ahead ten minutes.

What do you think? Have you encountered similar problems? What did you do to work around them?

No comments: