Testable Code is Better Code
It seems pretty well accepted these days that unit testing is preferable to not unit testing. Logically, this implies that most people believe a tested code base is better than a non-tested code base. Further, by the nature of testing, a tested code base is likely to have fewer bugs than a non-tested code base. But I’d like to go a step further and make the case that, even given the same amount of bugs and discounting the judgment as to whether it is better to test or not, unit-tested code is generally better code, in terms of design and maintainability, than non-unit-tested code.
More succinctly, I believe that unit testing one’s code results in not just fewer bugs but in better code. I’ll go through some of the reasons that I believe that, and none of those reasons are “you work out more bugs when you test.”
It Forces Reasoning About Code
Let’s say that I start writing a class and I get as far as the following:
public class Customer
{
public int Id { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
Pretty simple, but there are a variety of things right off the bat that can be tested. Can you think of any? If you don’t write a lot of tests, maybe not. But what you’ve got here is already a testing gold mine, and you have the opportunity to get off to a good start. What does Id initialize to? What do you want it to initialize to? How about first and last name? Already, you have at least three tests that you can write, and, if you favor TDD and don’t want nulls or zeroes, you can start with failing tests and make them pass.
It Teaches Developers about the Language
A related point is that writing unit tests tends to foster an understanding of how the language, libraries, and frameworks in play work. Consider our previous example. A developer may go through his programming life in C# not knowing what strings initialize to by default. This isn’t particularly far-fetched. Let’s say that he develops for a company with a coding standard of always initializing strings explicitly. Why would he ever know what strings are by default?
If, on the other hand, he’s in the practice of immediately writing unit tests on classes and then getting them to pass, he’ll see and be exposed to the failing condition. The unit test result will say something like “Expected: String.Empty, Was: null”.
And that just covers our trivial example. The unit tests provide a very natural forum for answering idle questions like “I wonder how x works…” or “I wonder what would happen if I did y…” If you’re working on a large application where build time is significant and getting to a point in the application where you can verify an experiment is non-trivial, most likely you leave these questions unanswered. It’s too much of a hassle, then the alternative, creating a dummy solution to test it out, may be no less of a hassle. But, sticking an extra assert in an existing unit test is easy and fast.
Unit Tests Keep Methods and Classes Succinct and Focussed
public void ChangeUiCar()
{
try
{
Mouse.OverrideCursor = Cursors.Wait;
MenuItem source = e.OriginalSource as MenuItem;
if (source == null) { return; }
ListBox ancestor = source.Tag as ListBox;
if (ancestor == null) return;
CarType newType = (CarType)Enum.Parse(typeof(CarType), ancestor.Tag.ToString());
var myOldCar = UIGlobals.Instance.GetCurrentCar();
var myNewCar = UIGlobals.Instance.GetNewCar(newType);
if (myNewCard.Manufacturer == "Toyota" || myNewCar.Manufactuer == "Hyundai" || myNewCar.Manufacturer == "Fiat")
{
myNewCar.IsForeign = true;
}
else if (myNewCar.Manufactuer == "Ford" ||
(myNewCar.Manufacturer == "Jeep" && myNewCar.WasMadeInAmerica)
|| (myNewCar.Manfacturer == "Chrysler" && myNewCar.IsOld))
{
myNewCar.IsForeign = false;
}
try
{
UpdateUiDisplay(myNewCar, true, false, 12, "dummy text");
}
catch
{
RevertUiDisplay(myOldCar, true, false, 0, "dummy text");
}
if (myNewCar.HasSunRoof || myNewCar.HasMoonRoof || myNewCar.HasLeatherSeating || myNewCar.HasGps ||
myNewCar.HasCustomRims || myNewCar.HasONBoardComputer)
{
bool isLuxury = CarGlobals.Instance.DetermineLuxury(myNewCar);
if (isLuxury)
{
if (myNewCar.IsForeign && myNewCar.IsManualTransmission)
{
myNewCar.DisplaySportsCarImage = true;
}
if (myNewCar.IsForeign)
{
myNewCar.DisplayAmericanFlag = false;
if (myNewCar.HasSpecialCharacters)
{
myNewCar.DisplaySpecialCharacters = UIGlobals.Instance.CanDisplayHandleSpecialCharacters();
if (myNewCar.DisplaySpecialCharacters)
{
UpdateSpecialCharacters(myNewCar);
}
}
else
{
UIGlobals.Instance.SpecialCharsFlag = "Off";
}
}
}
}
}
finally
{
Mouse.OverrideCursor = null;
}
}
This is an example of a method you would never see in an actively unit-tested code base. What does this method do, exactly? Who knows… probably not you, and most likely not the person or people that ‘wrote’ (cobbled together over time) it. (Full disclosure–I just made this up to illustrate a point.)
We’ve all seen methods like this. Cyclomatic complexity off the charts, calls to global state sprinkled in, mixed concerns, etc. You can look at it without knowing the most common path through the code, the expected path through the code, or even whether or not all paths are reachable. Unit testing is all about finding paths through a method and seeing what is true after (and sometimes during) execution. Good luck here figuring out what should be true. It all depends on what global state returns, and, even if you somehow mock the global state, you still have to reverse engineer what needs to be true to proceed through the method.
If this method had been unit-tested from its initial conception, I contend that it would never look anything like this. The reasoning is simple. Once a series of tests on the method become part of the test suite, adding conditionals and one-offs will break those tests. Therefore, the path of least resistance for the new requirements becomes creating a new method or class that can, itself, be tested. Without the tests, the path of least resistance is often handling unique cases inline–a shortsighted practice that leads to the kind of code above.
Unit Tests Encourage Inversion of Control
In a previous post, I talked about reasoning about code in two ways: (1) command and control and (2) building and assembling. Most people have an easier time with and will come to prefer command and control, left to their own devices. That is, in my main method, I want to create a couple of objects and I want those objects to create their dependencies and those dependencies to create their dependencies and so on. Like the CEO of the company, I want to give a few orders to a few important people and have all of the hierarchical stuff taken care of to conform to my vision. That leads to code like this:
class Engine
{
Battery _battery = new Battery();
Alternator _alternator = new Alternator();
Transmission _transmission = new Transmission();
}
class Car
{
Engine _engine = new Engine();
Cabin _cabin = new Cabin();
List _tires = new List() { new Tire(), new Tire(), new Tire(), new Tire() };
Car()
{
}
void Start()
{
_engine.Start();
}
}
So, in command and control style, I just tell my classes that I want a car, and my wish is their command. I don’t worry about what engine I want or what transmission I want or anything. Those details are taken care of for me. But I also don’t have a choice. I have to take what I’m given.
Since my linked post addresses the disadvantages of this approach, I won’t rehash it here. Let’s assume, for argument’s sake, that dependency inversion is preferable. Unit testing pushes you toward dependency inversion.
The reason for that is well illustrated by thinking about testing Car’s “start” method. How would we test this? Well, we wouldn’t. There’s only one line in the method and it references something completely hidden from us. But, if we changed Car and had it receive an engine through its constructor, we could easily create a friendly/mock engine and then make assertions about it after Car’s start method was called. For example, maybe Engine has an “IsStarted” property. Then, if we inject Engine to Car, we have the following simple test:
var myEngine = new Engine();
var myCar = new Car(myEngine);
myCar.Start();
Assert.IsTrue(myEngine.IsStarted);
After you spend some time unit testing regularly, you’ll find that you come to look at the new keyword with suspicion that you never did before. As I write code, if I find myself typing it, I think “either this is a data transfer object or else there better be a darned good reason for having this in my class.”
Dependency-inverted code is better code. I can’t say it any plainer. When your code is inverted, it becomes easier to maintain and requirements changes can be absorbed. If Car takes an Engine instead of making one, I can later create an inheritor from Engine when my requirements change and just give that to Car. That’s a code change of one modified line and a new class. If Car creates its own Engine, I have to modify Car any time something about Engine needs to change.
Unit Testing Encourages Use of Interfaces
By their nature, interfaces tend to be easier to mock than simple instances–even virtual ones. While I can’t speak to every mocking framework out there, it does seem to be a rule that the easiest way to mock things is using interfaces. So when you’re testing your code, you’ll tend to favor interfaces when all things are equal, since that will make test writing easier.
I believe that this favoring of interfaces is helpful for the quality of code as well. Interfaces promote looser coupling than any other way of maintaining relationships between objects. Depending on an interface instead of a concrete implementation allows decoupling of the “what” from the “how” question when programming. Going back to the engine/car example, if I have a Car class that depends on an Engine, I am tied to the Engine class. It can be sub-classed, but nevertheless, I’m tied to it. If its start method cannot be overridden and throws exceptions, I have to handle them in my Car’s start method.
On the other hand, depending on an engine interface decouples me from the engine implementation. Instead of saying, “alright, specific engine, start yourself and I’ll handle anything that goes wrong,” I’m saying, “alright, nameless engine, start yourself however it is you do that.” I don’t necessarily need to handle exceptions unless the interface contract allows them. That is, if the interface contract stipulates that IEngine’s start method should not throw exceptions, those exceptions become Engine’s responsibility and not mine.
Generally speaking, depending on interfaces is very helpful in that it allows you to make changes to existing code bases more easily. You’ll come to favor addressing requirements changes by creating new interface implementations rather than by going through and modifying existing implementations to handle different cases.
Regularly Unit Testing Makes You Proactive Instead of Reactive About the Unexpected
If you spend a few months unit testing religiously, you’ll find that a curious thing starts to happen. You’ll start to look at code differently. You’ll start to look at x.y() and know that, if there is no null check for x prior to that call, an exception will be thrown. You’ll start to look at if(x < 6) and know that you’re interested in seeing what happens when x is 5 and x is 6. You’ll start to look at a method with parameters and reason about how you would handle a null parameter if it were passed in, based on the situation. These are all examples of what I call “proactive,” for lack of a better term. The reactive programmer wouldn’t consider any of these things until they showed up as the cause of a defect.
This doesn’t happen magically. The thing about unit tests that is so powerful here is that the mistakes you make while writing the tests often lead you to these corner cases. Perhaps when writing the test, you pass in “null” as a parameter because you haven’t yet figured out what, exactly, you want to pass in. You forget about that test, move on to other things, and then later run all of your tests. When that one fails, you come back to it and realize that when null is passed into your method, you dereference it and generate an unhandled exception.
As this goes on over the course of time, you start to recognize code that looks like it would be fragile in the face of accidental invocations or deliberate unit test exercise. The unit tests become more about documenting your requirement and guarding against regression because you find that you start to be able to tell, by sight, when code is brittle.
This is true of unit tests because the feedback loop is so tight and frequent. If you’re writing some class without unit tests, you may never actually use your own class. You write the class according to what someone writing another class is going to pass you. You both check in your code, never looking at what happens when either of you deviate from the expected communication. Then, three months later, someone comes along and uses your class in another context and delivers his code. Another three months after that, a defect report lands on your plate, you fire up your debugger, and figure out that you’re not handling null.
And, while some learning will occur in this context, it will be muted. You’re six months removed from writing that code. So while you learn in principle that null parameters should be handled, you aren’t getting feedback. It’s essentially the difference between a dieter having someone slap his hand when he reaches for a cookie, or weigh him six months later and tell him that he shouldn’t have eaten that cookie six months ago. One is likely to change habits while the other is likely to result in a sigh and a “yeah, but are ya gonna do?”
Conclusion
I can probably think of other examples as well, but this post is already fairly long. I sincerely believe that the simple act of writing tests and getting immediate feedback on one’s code makes that person a better programmer more quickly than ignoring the tests. And, if you have a department where your testers are all writing tests, they’re becoming better designers/programmers and adopting good practices while doing productive work and raising the confidence level in the software that they’re producing.
I really cannot fathom any actual disadvantage to this practice. To me, the “obviously” factor of this is now on par with whether or not wearing a seat belt is a good idea.