Design For Testability

Design for testability

 

How can we recognize “Bad Design”? One could argue that a design can be qualified as “Bad” when it can’t be changed easily. When one small change in the code implies to make a lot of other changes or when the program break in many places when a single change is introduced. Because the parts are highly dependent on each other no effort will be invested in separating the modules that can be reused. Therefore a badly designed application will also tend to not be reused. The root of these symptoms is caused by the interdependencies between the building blocks of the application. The art of good design is to break these dependencies.

 

An interesting fact is that code that is interdependent is also difficult to test. That’s why our unit test are the most effective way to evaluate our design. Well designed applications made of loosely coupled parts will be easy to unit test. The opposite is also true, code that is easy to unit test is code that is generally well designed.  Unit testing is not about detecting existing defects but it’s an act of design. Unit tests help us in defining and evaluate our design. A piece of code that is difficult to test is a smell of bad design. When our code is difficult to test we should not try to write a test for it, we should first change it so it can be easily tested.

“If the answer is not obvious, or it looks like the test would be ugly or hard to write, then take that as a warning signal. Your design probably needs to be modified; change things around until the code is easy to test, and your design will end up being far better for the effort.” [Hunt, Thomas. Pragmatic Unit Testing in Java with JUnit.

In this series I describe what make our code hard to test therefore I provide several typical design anti-patterns  that makes our code hard to test and explain how these can be fixed. I provide also some patterns that can be applied in our SUT that can facilitate testing and enforce loose coupling.

 

Anti-patterns

 

1) The new keyword is used to construct anything you could replace with a test-double.

The most common cause making our test hard to test is violating the single responsibility principle. Look at the example here beneath, the SUT is responsible to construct his own collaborators. When your class has to instantiate and initialize its collaborators, the result tends to be an inflexible and prematurely coupled design. Such classes shut off the ability to inject test collaborators when testing. Do not create collaborators in your constructor or methods, but pass them in. (Don’t look for things! Ask for things!)

   1:  public class Invoice
   2:  {
   3:      private int _balance;
   4:   
   5:      public Invoice(int clientID)
   6:      {
   7:          DataLayer _db = new DataLayer();
   8:   
   9:          MeteringValues[] dailyValues = _db.GetMeteringValues(clientID);
  10:          int offPeakPrice = _db.GetOffPeakPrice();
  11:          int peakPrice = _db.GetPeakPrice();
  12:          int peakConsumption = CalculatePeakConsumtion(dailyValues);
  13:          int offPeakConsumtion = CalculateOffPeakConsumtion(dailyValues);
  14:          int advances = _db.GetAdvances(clientID);
  15:   
  16:          _balance = (peakConsumption * peakPrice +   
  17:                      offPeakConsumtion * offPeakPrice) – 
  18:                      advances;
  19:      }
  20:   
  21:      public int Balance
  22:      {
  23:          get { return _balance; }
  24:      }  
  25:      
  26:      private int CalculateOffPeakConsumtion(MeteringValues[] values)
  27:      {
  28:  
  29:      }
  30:   
  31:      private int CalculatePeakConsumtion(MeteringValues[] values)
  32:      {
  33:  
  34:      }
  35:  }

 

In the example here above, we can never replace the _db field with a test-double.  It’s true that the Invoice is easy to instantiate but this come at the cost of flexibility.  Because the DataLayer represents something expensive to access it is also not very testable .  To be able to inject a stubbed DataLayer into the Invoice we add a dataLayer parameter to the constructor:

   1:  public class Invoice
   2:      {
   3:          private int _balance;
   4:   
   5:          public Invoice(int clientID, DataLayer db)
   6:          {
   7:              MeteringValues[] dailyValues = db.GetMeteringValues(clientID);
   8:              int offPeakPrice = db.GetOffPeakPrice();
   9:              int peakPrice = db.GetPeakPrice();
  10:              int peakConsumption = CalculatePeakConsumtion(dailyValues);
  11:              int offPeakConsumtion = CalculateOffPeakConsumtion(dailyValues);
  12:              int advances = db.GetAdvances(clientID);
  13:   
  14:              _balance = CalculateBalance(
  15:                              peakConsumption, 
  16:                              peakPrice,  
  17:                              offPeakConsumtion, 
  18:                              offPeakPrice, 
  19:                              advances
  20:                         );
  21:          }
  22:   
  23:          protected int CalculateBalance(int peakConsumption, int peakPrice, int offPeakConsumtion, int offPeakPrice, int advances)
  24:          {
  25:              return (peakConsumption * peakPrice + 
  26:                      offPeakConsumtion * offPeakPrice) – 
  27:                      advances;
  28:          }
  29:   
  30:          protected int CalculateOffPeakConsumtion(MeteringValues[] values)
  31:          {
  32:  
  33:          }
  34:   
  35:          protected int CalculatePeakConsumtion(MeteringValues[] values)
  36:          {
  37:  
  38:          }
  39:   
  40:          public int Balance
  41:          {
  42:              get { return _balance; }
  43:          }
  44:      }

 

Here we dispose of a SUT that is a lot more testable because we are now able to inject a test doubles into the Invoice .  These test doubles can simply be a subtype based on DataLayer that returns default hard coded values.  A more advance technique is making use of a mocking framework to generate a stub/mock from the DataLayer class ->

 

 

  1: public void Construct_WithSampleValues_BalanceEqualsSampleBalance()
  2: {
  3:     //Arrange
  4:     var dalStub = MockRepository.GenerateStub<DataLayer>();
  5:     dalStub.Stub(m => m.GetMeteringValues(SampleClientID)).Return(SampleMeteringValues);
  6:     dalStub.Stub(m => m.GetOffPeakPrice()).Return(SampleOffPeakPrice);
  7:     dalStub.Stub(m => m.GetPeakPrice()).Return(SamplePeakPrice);
  8:     dalStub.Stub(m => m.GetAdvances(SampleClientID)).Return(SampleAdvances);
  9: 
 10:     //Act
 11:     var subject = new Invoice(1, dalStub);
 12:     
 13:     //Assert
 14:     
 15:     Assert.AreEqual(SampleBalance, subject.Balance);
 16: }
 17: 

 

Add comment