Unit Tests & Refactoring: Should I bother?
Once upon a time, a consultant made a visit to a development project. The consultant looked at some of the code that had been written; there was a class hierarchy at the center of the system. As he wandered through the hierarchy, the consultant saw that it was rather messy. A complex hierarchy that had several flaws in it from its very design. The consultant recommended to the project management that the code be looked at and cleaned up, but the project management didn’t seem enthusiastic. The code seemed to work and there were considerable schedule pressures. The managers said they would get around to it at some later point. The consultant had also shown the programmers who had worked on the hierarchy what was going on. So the programmers spent a day or two cleaning up the hierarchy. When they were finished, the programmers had removed half the code in the hierarchy without reducing its functionality. They were pleased with the result and found that it became quicker and easier both to add new classes to the hierarchy and to use the classes in the rest of the system. The project management was not pleased. Schedules were tight and there was a lot of work to do. These two programmers had spent two days doing work that had done nothing to add the many features the system had to deliver in a few months’ time. The old code had worked just fine. All this activity was devoted to making the code look better, not to making it do anything that it didn’t already do.
Six months later the project failed, in large part because the code was too complex to debug or to tune to acceptable performance.
Sound familiar? Well. Maybe not the “failed project” part, but I’m sure you can relate at some point. Software projects have a tendency to grow in complexity over time, either by the nature of the business or the several people working on it throughout its lifetime, each with his own views and background. But the common denominator between all “bad” software projects is… wait for it… lack of tests!
What do tests have to do with bad or good code, you ask? Uncle Bob expresses it the best:
With tests we can add new functionality easily. With tests we know if any change we did might introduce a bug. With tests we can express the intention of our code and let that be an additional part of the documentation! All that bad code is bad because it’s misunderstood, and tests help us understand it better.
Which brings us to the point: why aren’t we creating those tests? In most cases, it’s a habit. We’re used to tight deadlines and scheduling issues while dimensioning our work. We put very little time in thinking about unit testing our work, because we don’t see the value in it… Until we have to go back to it again, to fix a bug or to add more functionality…
To mitigate this, we need to change our habits.
The Force of Habit
What is a habit, anyway? It is some behavior that we go back to as a default, and it shows the best under stressful conditions. We are bombarded with situations, deadlines, expectations, that makes us go back to what we do automatically and let us deal with the rest, so we can reduce the uncertainty. It’s a safety mechanism of sorts.
But what if we had some habits that didn’t lead us down the path of ugly, unmaintainable code? What if we had habits that lead to code that people actually enjoy working with?
Beck usually responded to praises in such manner, because he wanted to express that we could all be good programmers. It’s just a matter of having the right habits.
And how do you build habits, you ask? Excellent question! Like I said before, habits are automatic responses, behaviors that we do unconsciously. These behaviors, we learned at some point in our life. We learned them by repeating them over and over, so we simply don’t think about them any more. So to build a new habit, we just keep on repeating its behavior until it’s engraved in our minds!
Testing and Refactoring as a Habit
Easier said than done, right? So how does one go around and learn this beautiful habit of Testing and Refactoring? Glad you asked (you’ve become pretty inquisitive at this point)! The easiest way to begin learning this new habit is to do some old TDD (Test Driven Design).
Although a full TDD approach is hard to find on most projects, it doesn’t mean that you can’t do it for yourself! In the end, the repetition of the TDD behavior will stay with you and you won’t even think about not having a test for your new and shiny piece of software. But let’s not get ahead of ourselves. To start down the TDD path, the way to go is to give it a shot with something simple and keep practicing as often as we can.
Practice, practice, practice
When we embark on the adventure of learning something new, the way to master it is practice. Starting small and building up from there. Stretching just a bit, so we can move on to more difficult things. We could split it into major types of practice: incidental and deliberate.
With incidental practice, we just repeat what we already know, over and over. This helps us make the activity we’re repeating an unconscious process and thus, a habit!
Deliberate practice is a different animal altogether. With it, we can actually learn something new. It makes us decompose the new skill into different little activities that we can, in turn, make into incidental practice. And we decompose the unknown into manageable chunks that we can understand and execute better. It helps us venture into the unknown with a little more confidence to keep on learning.
But how do these concepts relate to TDD? Simple. We can learn and practice our TDD skills with something called Code Katas.
Code Katas
In Karate, a Kata is a practice exercise. A set of movements and stances that are repeated over and over again, so they become second nature at some point. A Code Kata follows the same pattern. It is a relatively simple problem to be solved by a finite sequence of steps, which we can start repeating to make them part of our coding skills (Show me Sand the floor!).
When we solve the Code Katas with TDD we develop skills to split the problem ahead of us into smaller pieces, solving each one with the bare minimum and doing refactoring afterwards, to eliminate duplicity. All this while following the TDD mantra RED-GREEN-REFACTOR.
But Code Katas are not only meant to learn TDD. There are others that help you develop skills for Touch typing (i.e. I can type that with my eyes closed), IDE keyboard shortcuts (i.e. ctrl/cmd+shift+O…ff with my mouse!), SOLID principles, and many more!
TDD in a nutshell
Like I stated above, the TDD mantra goes RED-GREEN-REFACTOR. But how to start? From the very beginning! Our traditional way of working is to start by developing some classes here and there, putting in some code, wire things up, killing those nasty red squiggles, start the program and hope for it to work… And then we kind of, sort of start thinking about testing it. Maybe… if only I didn’t put that class here and that dependency there… Oh, well. I’ll fix it on the next release… if there’s time.
TDD would require a different approach. We start off by thinking about cases (what if?) that cover the expected functionality… without even having developed it!
Red-Green-Refactor
Let’s think of the most basic use case we can come up with for our new functionality. Create the test as if all the necessary components (classes, methods and properties) were already there, out of thin air. Here’s our first RED: this doesn’t even compile! So, let’s fix it. Red is our friend, so let’s listen to what it has to say. Undefined class? Create it. Undefined method or property? Create as well, as empty, returning null or uninitialized, depending on the case. Now it compiles! We fire up the test… Boom! Back to Red.
Now into Green. Make the simplest modification to make the test pass. This baffles everyone at the start, because we tend to over-engineer. We try to cover a lot of ground in one go, but for TDD that’s counterintuitive. Still, resist the urge and KISS (Keep It Simple, Stupid). Normally this simple modification would imply returning some fixed value that the test expects. And so, we run the test again and… Green!
We’re now into Refactor territory, but there ain’t that much code that could be duplicated, assuming your class/functionality is brand new. So no Refactoring for you this time!
Cool! Now we go for the next interesting case our code should manage. Again, code the test. If there are no red squiggles, run it. Back to Red! Note that if we don’t get a Red at this point, our assumptions of how this should work might be wrong (thank you, TDD!) and we need another more interesting test case since this one is already covered.
We make another simple modification to make the test pass again. A well placed if, perhaps? But do bear in mind that the previous test we already had must still pass!
So we made our changes and our tests pass. On to Refactoring… Our code still might not have enough lines to have duplicates to remove… But our tests might! Maybe refactoring the creation of the test subject by making it an instance field, and initialize it in the setup method. Now we’re keeping things tidy in both the production code AND the tests. And also making sure that we write only the code we need and nothing else! We’re writing code to satisfy the test case conditions (which reflect the requirements and even other edge cases), and not tests for the code we hope complies with the requirements.
We repeat this cycle a few more times. How many? Well, it all depends on the requirement you’re trying to fulfill. Once you run out of test cases, your feature is good to go! And get this! If you ever had a test case that you missed (a.k.a. an ugly Bug), all you have to do is get back into this cycle with this new test case! Also you get the certainty that whatever change you need to make for the case to work won’t break any of the existing functionality, because your tests have your back!
Conclusion
With a Testing oriented mindset we can deliver better code, that has more meaning. And with Refactoring we can create code that isn’t painful to look at. Better structured, non-redundant and tailored fit for the problem it needs to solve. By developing these skills we not only can become better programmers, but also become caring programmers. Professionals who care for the code they’re writing and the people who will come after we’re gone (yes… we’re not going to be around in that project forever!). It is a long road, but one totally worth traveling.