Sunday, 6 October 2013

Test Driven Education [part 2 of 4]

A full Test Driven Project to learn from.

In the previous post we've seen the development cycle of TDD and a first set of its important features which I want to recall once again:
  • It leads to the cleanest and most honest API ever.
  • It leads to the right and most effective balance of bottom-up and top-down design.
  • Unit tests make refactoring enjoyable rather than risky and scary.
  • Unit tests are quick to copy-paste-tweak letting us able to test paranoid cases.
In this second post we will appreciate the effectiveness of the Dependency Injection design pattern, naturally promoted by TDD. Its name should be quite self explaining: a component relying on other objects to get its job done, ask explicitly for instances of those objects via its API, rather than locate and instantiate them on its own. Although it might sound good having a component able to locate and instantiate collaborators on its own, it is not; because it leads to Software Architecture made of components tightly coupled between each other, making automatic testing a mission almost impossible which, in turn, reduce the quality of the end product. Dependency Injection, instead, enforces decoupling and modularization, which increases the quality of the end product. The unit tests themselves prove how modular the overall architecture gets: every single component is collaborating with at least two different implementations of its collaborators: the production one and the fake one.

To have a concrete proof of this, we're going to extend our Security System making SecurityAlarm interacting with burglar sensors and sirens. However, we'll first step back to the "old" times when we used to design software up-front and then we'll compare this with TDD.

Let's have a look at a couple of requirements:
  • When the alarm is engaged, the system shall activate all the sirens when at least one burglar sensor triggers.
  • The system shall provide hot plugging of sensors and sirens.
Let's now imagine what could have happened in the "old" times. We would have probably began by observing that the first requirement is pretty easy. We can have a vector of sensors and a vector of sirens. Periodically, we poll all the sensors and if any of them has triggered, then we iterate through the vector of sirens and activate them all. Then the Chief Architect might have raised his concern about performances and might have asked to implement an EventBus to asynchronously convey sensors' signals to SecurityAlarm. The second requirement might be slightly more complicated but for sure SecurityAlarm has to open the serial ports to scan for sensors and sirens. Then the Chief Architect might have raised again his concern, this time about portability, and introduced a SensorLocator which might use the EventBus to notify SecurityAlarm about new sensors being available. A couple of meetings (and design documents) might have followed with plenty of details about the EventBus and the SensorLocator: they'll be singletons, running on their own processes, the IPC will be implemented via message passing, etc. Only after a few days we might have been able to start the actual implementation of SecurityAlarm: it might have probably retrieved the instance of EventBus first, to subscribe itself in order to receive events; EventBus might then bring up the IPC mechanism by reading some config file; SecurityAlarm might also have retrieved the instance of SensorLocator to kick off a first scan for sensors; on its side, SensorLocator might read the config files and open all the serial, USB and I2C ports accordingly. Suddenly, we would have needed a full working system and our development process would have slowed down because of it.

However, we use TDD and we can start implementing those two requirements immediately. Let's formalize the first requirement with a test:

  public void testActivateTheSirenWhenTheSensorTriggers() {
    // setup
    class SpySiren implements Siren {
      boolean activated = false;
      @Override
      public void activate() {
        activated = true;
      }
    }
    SecurityAlarm alarm = new SecurityAlarm();
    SpySiren spySiren = new SpySiren();
    Sensor sensor = new Sensor();
    alarm.addSiren(spySiren);
    alarm.addSensor(sensor);
    alarm.engage("0000");
    // exercise
    sensor.trigger();
    // verify
    assertTrue("Siren not activated", spySiren.activated);
  }
This test gives us the opportunity to see how TDD naturally balances bottom-up and top-down approaches. We've started from the bottom-up, defining how we would SecurityAlarm to look like: we would like it to have two new methods addSiren() and addSensor() to pass in a Siren and a Sensor. Going now top-down,  Siren should be an interface providing an activate() method which SecurityAlarm will invoke, when the Sensor triggers. However, since Sensor does not yet exist, we'll now jump to define its requirement (which is still top-down, by the way).

public class SensorTest extends TestCase {
  public void testNotifySubscriberWhenTriggered() {
    // setup
    class SpyTriggerListener implements Sensor.TriggerListener {
      boolean notified = false;
      @Override
      public void triggered() {
        notified = true;
      }
    }
    Sensor sensor = new Sensor();
    SpyTriggerListener spyListener = new SpyTriggerListener();
    sensor.addTriggerListener(spyListener);
    // exercise
    sensor.trigger();
    // verify
    assertTrue("Subscriber not notified", spyListener.notified);
  }
}
Please note that while writing this test we have switched back to bottom-up, defining how Sensor should look like and defining the TriggerListener interface. The following is its implementation.
public class Sensor {
  static public interface TriggerListener {
    public void triggered();
  }
  private TriggerListener _listener;
  public void addTriggerListener(TriggerListener listener) {
    _listener = listener;
  }
  public void trigger() {
    _listener.triggered();
  }
}

Now that Sensor is working, we can go back implementing SecurityAlarm.

public class SecurityAlarm implements Sensor.TriggerListener {
  ...
  private Siren _siren;
  @Override
  public void triggered() {
    _siren.activate();
  }
  public void addSensor(Sensor sensor) {
    sensor.addTriggerListener(this);
  }
  public void addSiren(Siren siren) {
    _siren = siren;
  }
}

So far, we've almost implemented both the requirements and we now have everything in place to complete them. However, we'll skip over this, as it's just a matter of applying what we've learned in the previous post.

What is really important to note is that TDD has let the design emerge on its own. Sensor can now be extended to support serial ports, USB, I2C or more as the Chief Architect was desiring but without the need of an expensive up-front design. There is also no need to poll the sensors, which is another thing the Chief Architect would have needed to design explicitly. Moreover, Siren can now have different implementations and we might even turn on a spotlight or send an SMS or more; this is something that the Chief Architect didn't predict, but TDD let it emerge spontaneously.

In conclusion, the Dependency Injection design pattern increases the modularity of the Software Architecture for free and it enforces the decoupling between components; when using TDD, it is literally impossible avoiding it!

No comments:

Post a Comment