Individuals are good at writing code that only they understand, and even then only at that moment. I’ve returned to code I wrote the day before, only to scratch my head in befuddlement: “What in the world is this code doing?” Or, “What was I thinking?”
This tendency of code to look like hieroglyphics is a common problem, and it’s not insignificant or trivial. Code that isn’t clear can take orders of magnitude more time to comprehend, raising our maintenance costs through the roof. I’ve spent whole afternoons trying to decipher overly complex code. In contrast, my comprehension times on a comparable amount of well-factored code have been as low as five to ten minutes.
Developers have come up with many ways to try and fix this problem. The art of refactoring is about improving the design and readability of code. Instead of settling with something that “just” works, programmers look to rephrase code to make it more readable. However, I’m not sure that code can ever be completely self-documenting.
Other maintainability mechanisms include comments, design documents, and review processes. Out of these, I’ve found review processes to be the most valuable. The very act of reviewing requires that someone other than the author understand the code. All the refactoring, comments, and external documents are a waste of time if they too are inscrutable. At least if two people can understand the code, it’s probably reasonably readable.
Some developers use pair programming as an active, continual form of software review. Done properly (see Pair Programming Dos and Don’ts), pair programming can result in much higher system quality. One of the main reasons is that two heads are usually better than one. Two people working together tend to spot the flaws that someone working on their own might not notice.
More importantly, two people working together have to agree, and read the same things from the code as the other. Meeting this requirement, a requirement that pairing creates by definition, elevates communication to be the most important element of development. It’s no longer good enough that the code works–more than one person must be able to understand it.
Even using pairing as a tactic, meeting the goal of readable tests and code is a significant challenge.
One specific area of code comprehension is something I’ll refer to as “client comprehension:” given a specific class, how does a developer use it in client code? Interacting properly with a class requires answering many questions. A sampling of the things that often require answers:
- What is the right sequencing of method calls?
- What arguments should be passed?
- What happens if an argument is null?
- What state is the system in after methods are called?
- What happens if an exception is thrown?
Some developers prefer annotating the code with comments to help answer these questions. The trouble is, no guarantee exists that these comments are going to be readable or even accurate. Maybe unit tests can help answer some of these questions with more accuracy.
Unit Testing vs. Test-Driven Development
Many developers are now in the habit of writing unit tests against their code. Unit tests are small bits of code that verify whether or not chunks of production logic work properly. Most typically, programmers write their code first, then come back and write unit tests against the completed code.
In general, the more unit tests that programmers write, the more likely that they find or prevent defects. However, coding and maintaining unit tests requires a significant investment. So significant, in fact, that it doesn’t make sense to me to “just” get better test coverage, and thus hopefully fewer defects, out of it.
If I’m going to put all that effort into coding unit tests, I believe that I need to get more things out of doing so. One thing I hope for is that the unit tests can act as some level of comprehensive documentation against class capabilities.
In fact, I have been able to get this return on investment from unit tests, through practicing test-driven development (TDD). TDD is a form of unit testing where developers code in a brief cycle:
- write a unit test, or even a piece of a unit test
- verifying its failure using a simple tool (the code doesn’t exist yet)
- write code to meet the specifications of the unit test
- verify the code’s correctness using the tool
- refactor the code, eliminating any code quality problems that were just introduced
- verify the code’s correctness to demonstrate that refactoring broke nothing.
I’ve gotten some other important benefits out of doing TDD, including obtaining a better, more maintainable design. My debugging cycles have been shorter, not just fewer, a benefit I have gotten out of doing “just” unit testing. Since doing TDD generally results in higher level of test coverage, it’s brought me the ability to refactor code with high levels of confidence that I’m not breaking anything. TDD also allows me to develop with a consistent rhythm, something that trickles up to help improve my estimating ability.
In contrast, I’ve gotten few of these benefits out of doing just unit testing (aka “test-after development,” or “TAD,” as in “a TAD too late”). In fact, I’ve sometimes questioned the value of doing TAD at all, given the much lower return on value and high cost of maintaining the tests.
One of the significant mindsets that goes along with doing TDD is thinking about how they drive and define what a class does. For every feature, every use of a class that is needed, a programmer drives the implementation of this feature by adding a unit test. The unit test is named appropriately. An example of a concisely-named unit test might be “testAccountDebitedOnWithdrawal,” or “testAccountFrozenWhenFundsInsufficient.” These features are expressed and developed from the perspective of the client. In fact, the tests are technically the first client of the class under test.
The ability of tests to act as documentation is an important consideration in TDD. TDD-style unit testing forces programmers to continually think about how the code should be used in order to accomplish a task. Contrast that to the mindset of unit testing, which is, “does the code work as written?”
Still, it’s possible to write unit tests that read poorly. As with code, I’ve written unit tests that made little sense the next day. In fact, it’s fairly easy to write tests that correctly verify code, but provide no answers to a future maintainer.
As mentioned earlier, a review process can help improve the quality of tests and product code considerably. If someone else can read my code, chances are it’s probably ok.
One technique that I’ve found extremely valuable is the idea of paraphrasing your tests. You can use this technique regardless of whether you have a formal review process. Even if you are pairing, it can be worthwhile to involve a third party.
The idea of paraphrasing is that someone not involved in the creation of the code reads the test out loud. “OK, in testFreeServiceWithinWarrantyPeriod, first it looks like you’re creating a customer object. You populate it with some basic data, then you set the installation date to a year ago plus a day. Then you call the warrantyService method on it. Finally you prove that the customer charge is $0. Makes sense!”
Often, the paraphraser will say something like, “I’m not sure why you get a charge of $100, based on the input data.” Someone reading a test shouldn’t have to ask these kinds of “why” questions. Any such questions indicate that the test needs to be refactored. Perhaps the name of the test needs to be revisited. Often, magic numbers (string or numeric literals embedded directly in the code) need to be replaced with appropriately-named constants. Variables might need to be renamed, the code might need to be decomposed better, and so on.
Paraphrasing is a great technique that developers can employ easily and cheaply. Even a solo developer can read the test out loud, listening to how it sounds. I’ve employed this technique on my own, and found it has improved the quality of my tests dramatically. I highly recommend test paraphrasing as a step to introduce prior to code check-in.