People who know me personally know that I like to go for a long run on a regular basis. I sit and work at a desk all day. So as part of the work I do, labouring the codes, I go for a one or two hour run every other day. I already wrote about this in the past.
Besides the obvious benefits one gets from physical exercise, I also learned something quite valuable. I learned how to listen to my body. Every runner knows that running long distances involves some kind of pain. Over time, one develops a certain threshold for enduring this suffering. I know this doesn’t sound like much fun, but it really is. You can take my word for it 😀.
But over time I learned how to read the signals that my body sends me. These signals can be very subtle. How easy is it to breathe and get air? How are my legs feeling? Do my muscles feel strained? If so, to what degree? I’m constantly evaluating. For example, after a minute or two I know exactly whether I’m able to go for a fast run or that I should take it rather slowly. Even on a number of occasions, I was able to predict upcoming health issues or injuries. In such case, I have the option to reduce the intensity of a workout or even stop altogether. In any case, when something’s up, I have to act accordingly and prevent worse.
I have a similar experience when I’m writing code using Test-Driven Development. I write a small, failing test. I make the test pass as quickly as possible. And then the most important step: I refactor. Very short, successive cycles where each cycle shouldn’t take up more than just a couple of minutes. But why do I point out the “Refactor phase” as the most important one? Because this is the moment where I listen to what the unit tests are trying to tell me. Just as listening to the signals of one’s body is the most important part of running long distances, the same holds true for the whole Test-Driven Development process. In order to write sustainable software, we as developers have to learn about how to be receptive to these signals. But what exactly should we listen for?
Experienced developers often tell you that unit tests drive the design, and in a sense that’s true. But there’s an important step that comes before that. First and foremost, unit tests provide very valuable feedback about the design of the system. From this feedback, design decisions start to emerge. Test-Driven Development can be very unforgiving when the code quality of the system under test is quite poor. When it takes a long time to write a single, failing unit test, then it’s already telling us that we need to take some things into consideration during the refactor phase. From the “Red, Green, Refactor” cycle, the “Red” and “Green” stages should move as quickly as possible. The “Refactor” stage can take bit longer.
This is usually the point where newcomers to Test-Driven Development are put off by this discipled practice. The “Refactor” stage is oftentimes reduced or skipped altogether. And as soon as it becomes difficult to write and maintain unit tests, they shoot the messenger. They blame the test themselves instead of listening and blaming the design of the system being tested. Just as newcomers to running often blame the excessive pain they endure instead of just acting according to the signals their body is sending them along the way. Bad design of the system leads to brittle unit tests of poor quality.
Here’s a small example to illustrate this.
public abstract class CustomerHandlers
{
public void Handle(RemoveCustomer command)
{
// Remove a customer ...
}
}
public class RegularCustomerHandlers : CustomerHandlers
{
public void Handle(CreateRegularCustomer command)
{
// Create a new regular customer ...
}
}
public class RegularVipCustomerHandlers : CustomerHandlers
{
public void Handle(CreateVipCustomer command)
{
// Create a new VIP customer ...
}
}
We have a system that models two different types of customers: a regular customer and a VIP customer. Creating one of these involves different kind of business logic, but removing a customer is the same for both types. The developer that implemented this functionality decided to create a different handler class for each type of customer. These specific handler classes in turn derive from an abstract base class that provides the implementation for removing a customer. Let’s have a look at the unit tests.
[TestFixture]
public class RegularCustomerHandlersTests
{
[Test]
public void TestScenario01ForRemoveCustomer()
{
// Test scenario 1 for removing a customer
}
[Test]
public void TestScenario02ForRemoveCustomer()
{
// Test scenario 2 for removing a customer
}
[Test]
public void TestScenarioForCreateRegularCustomer()
{
// Test scenario for creating a regular customer
}
}
[TestFixture]
public class VipCustomerHandlersTests
{
[Test]
public void TestScenario01ForRemoveCustomer()
{
// Test scenario 1 (duplicate) for removing a customer
}
[Test]
public void TestScenario02ForRemoveCustomer()
{
// Test scenario 2 (duplicate) for removing a customer
}
[Test]
public void TestScenarioForCreateVipCustomer()
{
// Test scenario for creating a VIP customer
}
}
A test fixture has been used for both concrete handler classes. But notice that both test fixtures contain identical unit tests for removing a customer. This is one example where the design of the production code somewhat looks reasonable for a developer, but where the tests are claiming otherwise.
Suppose that we need to make a change to the functionality of removing a customer. If we want use Test-Driven Development, which one of these unit tests should we change first. Those in the RegularCustomerHandlersTests, or in the VipCustomerHandlersTests or both? This smells rather fishy.
Also the developer must have noticed that it somehow wasn’t that easy to write unit tests for the removal functionality. The abstract base class cannot be instantiated, so a concrete class must be used in order to invoke the Handle method. Which one should be chosen? The RegularCustomerHandlers class or the VipCustomerHandlers, or maybe a creating a third one specifically for testing? In the end, probably one of the two has been chosen. In order to make up for the bad feeling, the unit tests for removing a customer have been copied over to the test fixture of the other handler once its functionality has been finished.
And this is what we end up with when we do not properly pick up the signals that unit tests are broadcasting. A slightly better design could be the following:
public class RemoveCustomerHandler
{
public void Handle(RemoveCustomer command)
{
// Remove a customer ...
}
}
public class CreateRegularCustomerHandler
{
public void Handle(CreateRegularCustomer command)
{
// Create a new regular customer ...
}
}
public class CreateRegularVipCustomerHandler
{
public void Handle(CreateVipCustomer command)
{
// Create a new VIP customer ...
}
}
Here we have a specific handler class for each command. Likewise the unit tests now look like this:
[TestFixture]
public class RemoveCustomerHandlerTests
{
[Test]
public void TestScenario01ForRemoveCustomer()
{
// Test scenario 1 for removing a customer
}
[Test]
public void TestScenario02ForRemoveCustomer()
{
// Test scenario 2 for removing a customer
}
}
[TestFixture]
public class CreateRegularCustomerHandlerTests
{
[Test]
public void TestScenarioForCreateRegularCustomer()
{
// Test scenario for creating a regular customer
}
}
[TestFixture]
public class VipCustomerHandlerTests
{
[Test]
public void TestScenarioForCreateVipCustomer()
{
// Test scenario for creating a VIP customer
}
}
Here we have a dedicated test fixture for each handler class.
When it’s difficult to write a unit test, it hints us that the production code needs to be changed. We need to refactor the code so that it becomes easy to test. Production code that is very easy to test, that allows us to write a unit test in just minutes or even seconds, is code that is responsive to change. This is what we should strive for.
But how can we learn to listen? Unfortunately, we can only learn this by doing. Practice, practice and then practice some more. There are plenty of code katas out there. It can not be overstated how important it is to constantly practice outside of the typical work scenarios. But also rigorously apply Test-Driven Development in your daily work as well. The same goes for running. Going out for a workout on a regular basis is how you can learn about yourself, what you’re physical capabilities are, and most importantly what you’re (currently) not capable of. This is how you can improve. Going for longer distances or running faster. This is how you can move forward.
Just as important as learning about Test-Driven Development, is to learn about software design as well. Learning about both at the same time should go hand in hand. When you learn how to pick up the signals from your unit tests, you’re bound to learn something about the design of the code as well. Try out different design approaches. Keep it going. All the time.
Top comments (0)