A recent editorial -- "Is unit testing beneficial?" -- cited a number of reasons for why developers might not consider unit testing a priority:
- They don't know about it.
- Good unit tests are hard to write.
- It's a waste of time and productivity.
- Writing the tests would take too long (especially if they're doing frequent iterations).
- Regression testing is more effective.
Focusing on just the first two objections, how valid are they as reasons (rather than excuses) for developers to avoid unit testing?
There are many definitions of what constitutes unit testing, but most of these agree on at least one thing: Unit tests are the responsibility of the programmer. Unit testing comes under what is commonly called programmer testing. (Note that where system testing is testing of a system, programmer testing is testing by a programmer, not of a programmer.)
Even before we consider what a unit may or may not, perhaps the first consideration is whether it is reasonable to expect programmers to test their own work. Are programmers responsible for the work they do, or is that somebody else's problem?
In recent years agile processes have popularized the role of testing throughout the lifecycle and across different roles in development. The idea that programmers are responsible for testing the code they write, however, is far from new and finds itself expressed by voices from many different traditions and disciplines of development and testing.
For example, in "Critical Testing Processes," Rex Black defines unit testing as programmer testing:
I find the projects I work on usually go more smoothly when programmers do some unit and component testing of their own code. Through the ascendance of approaches like Extreme Programming, such a position is becoming less controversial. [...] So, a good practice is to adopt a development process that provides for unit testing, where programmers find bugs in their own software, and for component testing, where programmers test each other's software.
In Grady Booch's classic "Object-Oriented Analysis and Design with Applications," the following view of responsibilities and kinds of tests is offered:
Unit testing involves testing the individual classes and mechanisms; is the responsibility of the application engineer who implemented the structure. [...] System testing involves testing the system as a whole; is the responsibility of the quality assurance team.
Although agile thinking generally favors a more collocated style of system testing, even this perspective from the early 1990s is clear in placing responsibility for testing the code with those who wrote the code. As Steve Maguire noted in "Writing Solid Code" around the same time:
Programmers today aren't sure their code is bug-free because they've relinquished responsibility for thoroughly testing it. It's not that management ever came out and said, "Don't worry about testing your code—the testers will do that for you." It's more subtle than that. Management expects programmers to test their code, but they expect testers to be more thorough; after all, that's Testing's full-time job.
What seems fairly clear is that even without debating what does and does not constitute a unit test, there is a well-established rather than novel expectation that programmers carry out some sort of testing on their own code. This does not overlap, conflict with, or displace the separate responsibility that someone needs to take for testing the system as a whole.
Know your units
Knowing that programmers should test is not the same as knowing what and how to test. In spite of its popularity as a term, unit testing is far from having a single, clear-cut definition. Sometimes this doesn't matter — for example, when motivating programmer testing in general — but sometimes it does — for example, in deciding what constitutes a good unit test (or when debating the merits of unit testing).
The broadest definition of unit testing equates it with programmer testing. The most circular definition equates unit testing with anything that can be tested with a self-proclaimed unit-testing framework, e.g., any of the xUnit family of frameworks. The current trend is for crisper, more focused definitions that relate unit testing to design qualities of the code. From this point of view, a unit test's outcome is strictly determined by code correctness (the correctness of both production code and test code) in isolation from interaction with external dependencies. This underscores the relationship between unit testability and dependency management.
One reason, therefore, that good unit tests may be hard to write is a reflection of design quality issues in the code being tested. Tightly coupled code is harder to test than loosely coupled code. This is not so much a reason to avoid unit testing, as a reason to improve the architecture. Unit testing will most likely not be the only indication of the problem — there will probably be other project and code-quality indicators that suggest the architecture is not being all that it could be. Of course, there may also be political and economic reasons why the overall system quality cannot be improved, but it is important to recognize in such cases that the obstacle to unit testing is in the organization and in the production code, and it is not unit testing itself.
Another reason that unit tests may be hard to write is simply one of practice and skill. Given that the majority of programmers do not test their own code, are never formally taught how to test, never pick up a book or read blogs about unit testing, and rarely encounter examples of good unit tests, it is likely that a lack of know-how is a major contributor to the perception that unit tests — especially good ones — are hard to write.
But surely writing test code in Java using JUnit to test production Java code requires no more than learning the relevant syntax for writing tests? A programmer already versed in Java should be able to pick that up in no time! Well, yes and no. It is likely that they can master the necessary Java syntax, but good unit tests are more than just a ragtag collection of test cases filled with assertions. Unit tests are quite different in style and intent to the code that they test, so there is no guarantee that a programmer skilled in writing production code will be able to write comparably good unit tests.
Without recognizing that unit testing is a skill distinct from writing production code, and without additional prompting, many programmers invariably plateau at a procedural style of testing. Their test cases line up one for one with individual methods on a class or functions in a file — testFoo tests method foo, testBar tests method bar, and so on. Indeed, such a style is unquestioningly encouraged by test-stub generators found in a variety of IDEs, but it should not remain unquestioned. This style is not only tedious; it also tends to deemphasize distinct behaviors, such as boundary cases and error cases. All in all, a body of test code built in this procedural is unlikely to qualify as either good or enjoyable. A more behavioral approach to unit testing is recommended and has the additional quality of documenting in executable form the key intentions of the code under test.
As with any style or approach, to become good at unit testing typically requires recognition that it is a skill, identification of a body of knowledge that defines good practice, and last, but not least, practice. To object to doing something at all because it is hard to do well seems a curious and somewhat dubious justification. Of course, this does not automatically make the case for doing something badly — poor unit tests can be worse for a project than no unit tests at all — instead it highlights an opportunity for improvement.
About the author: Kevlin Henney is an independent consultant and trainer based in the UK. His work focuses on software architecture, patterns, development process and programming languages. He is a coauthor of A Pattern Language for Distributed Computing and On Patterns and Pattern Languages, two recent volumes in the Pattern-Oriented Software Architecture series. You may contact him at firstname.lastname@example.org.