Saturday, January 23, 2010

Primer - How To Write Testable Code

This is the first article in the Primer series. The Primer series will mostly be devoted to practical advise on various topics in software architecture and development.

This week I'll show you how to write code that is SOLID and testable. The reason for writing testable code is, of course, maintainability of your application. Maintainability results in fewer issues therefore lower true cost of ownership. Needless to say, it is one of the key considerations for any architect.

Before you start writing unit tests and integration tests and automating it all in the continuous integration environment, you have to make sure that the technical design principles you apply make your code easily testable. If you can't cleanly test your code your tests will not have any value as bugs will be easy to miss or tests would end up being to fragile. Whether you apply test driven development (i.e. write tests first) or not, the complexity of your technical design will ultimately affect the complexity of your tests.

Here are a few suggestions to follow, before you even start:
  • Follow the SOLID principles. They will ensure that you reduce complexity by creating single-responsibility classes and therefore reduce complexity of your tests as you'll be testing one responsibility at a time.
  • Apply the inversion of control pattern. Inversion of dependencies will allow you to build up the mocks more easily when writing unit tests.
I'll demonstrate some of these principles on an example. The example is an airline ticket ordering framework.

I'll start by defining the public API of the framework, that consists of a few interfaces and classes.


public interface IOrder
{
  OrderConfirmation Submit(string flightNumber, AccountInformation account);
}


public interface IFlightSchedule
{
  FlightInformation GetFlightInformation(string flightNumber);
}


public interface IReservations
{
  Reservation Reserve(FlightInformation flightInformation);
  void ConfirmReservation(Reservation reservation);
  void CancelReservation(Reservation reservation);
}


public interface IBilling
{
  Invoice Charge(AccountInformation account);
}


public class AccountInformation
{
  public AccountInformation(
    string creditCardNumber,
    string expirationDate,
    string fullName,
    string address)
  {
    CreditCardNumber = creditCardNumber;
    ExpirationDate = expirationDate;
    FullName = fullName;
    Address = address;
  }


  public string CreditCardNumber { get; private set; }
  public string ExpirationDate { get; private set; }
  public string FullName { get; private set; }
  public string Address { get; private set; }
}


public class FlightInformation
{
  public FlightInformation(
    string flightNumber,
    DateTime departureDateTime,
    DateTime landingDateTime,
    string origin,
    string destination)
  {
    FlightNumber = flightNumber;
    DepartureDateTime = departureDateTime;
    LandingDateTime = landingDateTime;
    Origin = origin;
    Destination = destination;
  }


  public string FlightNumber { get; private set; }
  public DateTime DepartureDateTime { get; private set; }
  public DateTime LandingDateTime { get; private set; }
  public string Origin { get; private set; }
  public string Destination { get; private set; }
}


public class Reservation
{
  public Reservation(string reservationId, string flightNumber, string seat)
  {
    ReservationId = reservationId;
    FlightNumber = flightNumber;
    Seat = seat;
  }


  public string ReservationId { get; private set; }
  public string FlightNumber { get; private set; }
  public string Seat { get; private set; }
}


public class OrderConfirmation
{
  public OrderConfirmation(FlightInformation flight, Invoice orderInvoice, Reservation flightReservation)
  {
    Flight = flight;
    OrderInvoice = orderInvoice;
    FlightReservation = flightReservation;
  }


  public FlightInformation Flight { get; private set; }
  public Invoice OrderInvoice { get; private set; }
  public Reservation FlightReservation { get; private set; }
}

First notice a few things. The business components that make up my public API are abstracted as interfaces. I always make all my public API interface-based. Secondly, all the classes that are part of the public API are entity-like classes and are intentionally written to be immutable. That way if you get an object from the business component interface you know it hasn't been modified. Notice also how I factored the business logic of a ticket order into separate resposibilities: schedule, reservation, billing and order itself. This is done to reduce complexity and make each of these individual components more testable.

Now let's look at the implementation of the Order component:

internal class Order : IOrder
{
  private readonly IFlightSchedule m_flightSchedule;
  private readonly IReservations m_reservations;
  private readonly IBilling m_billing;


  public Order(IFlightSchedule flightSchedule, IReservations reservations, IBilling billing)
  {
    m_flightSchedule = flightSchedule;
    m_reservations = reservations;
    m_billing = billing;
  }


  #region IOrder Members


  OrderConfirmation IOrder.Submit(string flightNumber, AccountInformation account)
  {
    FlightInformation flightInfo = m_flightSchedule.GetFlightInformation(flightNumber);


    Reservation reservation = null;


    try
    {
      reservation = m_reservations.Reserve(flightInfo);


      Invoice invoice = m_billing.Charge(account);


      m_reservations.ConfirmReservation(reservation);


      return new OrderConfirmation(
flightInfo,
invoice,
reservation);
    }
    catch (BillingException)
    {
      m_reservations.CancelReservation(reservation);


      throw;
    }
  }


  #endregion
}
 
Now, clearly, in order to complete an order, the Order component must communicate with other related components: flight schedule, reservations and billing. This creates dependencies between Order and the other components. These dependencies are, however, inverted in the design of the Order class. Notice that Order has references to IFlightSchedule, IReservations and IBilling and those are passed to Order in the constructor. This way I'm ensuring that Order doesn't know anything about the implementation of these components and that it is given the implementation through the constructor. This will also allow for dependency injection, constructor based injection is preferred over property injection as it keeps the API of the Order class immutable as well. Order class, as well as all other implementation of the public API, is made internal so it is hidden.
 
In the implementation of the IOrder interface, the Order class simply calls the dependent components, handles the business logic of it, and builds up the result OrderConfirmation object.
 
The way you would work with this framework is by getting an IOrder from it somehow. Now, there are a number of ways you can implement this, dependency injection framework being one, handcoded factory being another. I prefer to handcode the factory unless I need the flexibility of re-wiring the components at deployment/configuration time.
 
Let's say you want to test the Order class. There's only one method you need to test here, Submit method. It's business logic can be desribed as: get flight information, try to create reservation, if it fails bail out, else continue on to charge the credit card, if it fails cancel reservation, else confirm it, and return order confirmation. Pretty simple. When writing the unit test for this method, what you want to do is mock all dependent components: IFlightSchedule, IReservation and IBilling. You can use any of the mocking frameworks out there. Make sure to set up the mocks to validate the calls and input parameters to ensure that Order calls the right methods and passes the right things. In this case the business logic is such that it makes sense to validate the sequence of those calls to mocks, too.
 
This was a very simple example, but it demonstrates some of the key principles to follow to make your code more testable. In reality, these concepts are mostly applied when implementing business layers in applications as those are often most complex due to the complex nature of the business logic they encapsulate. Same principles apply, though, when communicating across layer boundaries.

No comments:

Post a Comment

Post a Comment