It’s a commonplace to say there is no “silver bullet,” and everything we do in the software field has to take context into consideration. In fact, this is quite true. TDD is a useful technique to know. To know TDD well, you must understand why and when it is useful, and how to do it correctly. If you apply TDD for the wrong reasons, in the wrong places, or in the wrong way, then it will not yield any value.
Many of the complaints people raise about TDD and about unit testing in general boil down to a misunderstanding or a misapplication of practices. Some complaints, however, are completely valid. You have to make your own professional judgments about such matters. To be equipped to make such judgments, you need to understand how TDD can add value in your work; and when it will not.
Hype for and against TDD
It can seem as if proponents of TDD believe that if we would all test-drive our code, there would be no defects and no difficulties in maintenance. In reality, TDD is one technique (or two, if you consider the two styles as different techniques) that helps us gain confidence that our code is complete and functionally correct.
Just as some proponents may seem to oversell TDD at times, some detractors go to the opposite extreme. They suggest that TDD is completely useless, because it does not absolutely guarantee completeness and correctness. Of course, nothing else guarantees that, either, but detractors prefer not to think about that small fact.
Some detractors dismiss TDD and unit testing generally because these practices don’t apply to every type of software or to every part of an application (for example, UI code). Their assumption appears to be that one is expected to go to extreme lengths to force unit tests into the solution, even when they don’t add value or aren’t practical. As that sort of extremism is not useful, they want to dismiss the practice in all circumstances.
There is no single technique that absolutely assures our code is correct. Therefore, it’s sensible to use multiple techniques to maximize our confidence. TDD has a role to play in that.
Lean-Agile Principles and TDD
To establish a baseline of ideas against which to measure the usefulness of TDD, let’s reiterate a short set of principles associated with a contemporary school of thought in the software field known as lean-agile development.
1. Take an economic view
2. Apply systems thinking
3. Assume variability; preserve options
4. Build incrementally with fast, integrated learning cycles
5. Base milestones on objective evaluation of working systems
6. Visualize and limit WIP; reduce batch sizes; manage queue lengths
7. Apply cadence; synchronize with cross-domain planning
8. Unlock the intrinsic motivation of knowledge workers
9. Decentralize decision-making
Let’s see where these ideas came from, what they mean to a software developer, and where TDD ties into them.
Take an economic view means to assess practices, processes, tools, frameworks, activities, etc. in terms of their economic impact to customers, the organization, the team, and yourself. The idea has sources in both the agile and lean schools of thought. A number of different practitioners, authors, consultants, and trainers, have arrived at similar conclusions after coming at the issue independently and from different directions.
By taking an economic view of software development activities, we can easily either confirm or debunk most of the criticisms people raise about TDD and unit testing. For example, some objections to TDD simply state that writing the unit tests is additional work above and beyond writing the application code. From an economic standpoint, the time saved by having automated unit checks in place at the outset far outweighs the amount of effort expended in writing the test cases. That effort doesn’t represent “extra” work so much as it represents an alternative way to achieve the same results as after-the-fact manual testing, manual documentation, time spent in the debugger, and visual analysis of code to discover what an existing system actually does.
Apply systems thinking. Systems Thinking has a long history and a wide range of applications. It’s a very practical and useful way to examine software development and delivery methods. It doesn’t directly lead us to TDD or automated unit testing, but it does help us see our work in a broader context than merely the number of keystrokes we must execute to deliver code.
Detractors of TDD and automated testing often appear to take a highly local view of the world; quite the opposite of Systems Thinking. I may have to type a few “extra” keystrokes, at least initially, and therefore I don’t want to do this thing. My job is to deliver code and then run away as fast as I can. I don’t care how hard or expensive it is to change the code later. When we consider the obvious and non-obvious ripple effects of slowing down development by not preparing appropriate automated functional checks, it’s hard to overlook the value of TDD compared to the “extra” effort.
Assume variability; preserve options means to approach our work with the assumption that things will change. To be able to absorb change, we must not lock ourselves into decisions made early in the game, or we will have a difficult and expensive road ahead when we have to change things. The concept of embracing change is fundamental to the agile school of thought, and the idea of deferring decisions until the last responsible moment is common to both lean and agile thinking. Part of the rationale for doing iterative/incremental development is to preserve our options; to make change easier and cheaper than it would be using traditional methods. The idea of real options has had some play in the lean and agile space, as well, although it is not used in a rigorous way in that community anymore. All this ties back to the idea that we want to be able to embrace change when it occurs, without excessive cost.
TDD and automated unit tests help us achieve that goal in several ways. First, the test cases point us to the specific places in the code where changes must be made in order to implement a new feature or modify an existing one. Second, the incremental refactoring that is part and parcel of the TDD approach ensures the design of the application will be relatively clean, and less likely to contain hidden surprises than code written in the traditional way. Third, the test cases provide a form of documentation that can’t be out of sync with the code, as we don’t modify the code except for the purpose of making a test case pass; not only when the code is first written, but every time it is changed.
Build incrementally with fast, integrated learning cycles expands upon and emphasizes the idea of preserving our options so that we can embrace change. It emphasizes the nature of software development as a learning activity, as opposed to a mechanical process to generate lines of code. TDD supports this idea as it leads us naturally to build our code in very small steps. Any change or reversal then becomes quick, easy, and cheap, and incremental refactoring remains feasible as we learn more about the domain and the solution during the development process.
Base milestones of objective evaluation of working systems is a more-robust way of stating the same thing the Agile Manifesto says when it mentions “…we have come to value working software over comprehensive documentation.” One way we can tell objectively whether a system is working is to exercise the code regularly through automated functional checks at multiple levels of abstraction. TDD is an effective way to establish and maintain such checks without requiring duplicate effort to create test suites after the fact.
Visualize and limit WIP; reduce batch sizes; manage queue lengths are restatements of fundamental lean ideas. The terse statement of principle sit atop a significant amount of work by a large number of people over many years in a range of contexts. There’s too much substance behind the statement to dissect it all here.
The way TDD contributes to supporting this principle is by helping us decompose our work into very small pieces. It’s easier to swap work items as priorities and requirements change when the work items are small than when they are large. It’s easier to achieve continuous flow with low WIP when the individual work items are small than when they are large. It’s easier to see how much work is actually hiding in a queue, awaiting someone’s attention, when the work items are small and reasonably same-sized than when they are large and variable in size.
Apply cadence; synchronize with cross-domain planning is more relevant to managing the overall work flow than to TDD directly. Even so, TDD is one of several practices we can employ that help us work on a steady cadence by leveling out the sizes of work items.
Unlock the intrinsic motivation of knowledge workers is an idea whose sources include psychology, sociology, neuroscience, organizational improvement methods, and emergent management models as well as basic agile (self-organization) and lean (respect for people) thinking. TDD supports emergent design, which in turn requires developers to think actively and continually about the validity and quality of the software design. Working in this way helps bring three key values to the workplace, as identified by popular author Daniel Pink in his book, Drive: Autonomy, mastery, and purpose.
Decentralize decision-making means to push decision-making authority (and associated responsibility) downward and outward in the organization. The idea has gained traction with the growing popularity of Beyond Budgeting concept, which is promoted by the Beyond Budgeting Roundtable. It is a key component of 21st-century organizations and management thinking as described by the likes of Steve Denning, Jurgen Appelo, Frederic Laloux, and Niels Pflaeging. It comes down to the recognition that Soviet-style corporate management doesn’t work any better than centrally-planned Five Year Plans.
TDD does not directly support the idea of decentralized decision-making. However, in context TDD is usually applied in conjunction with several other practices. Collectively, these practices do support decentralized decision-making in the limited domain of software architecture and design. TDD, pairing or mobbing, delivery of vertical slices of functionality, incremental development, team collaboration, and other modern practices tend to pull the decision-making power from functional silos like Architecture and Database Administration and inject it into development teams. Even if this is not done deliberately, it turns out that the speed of delivery that an enabled agile team can achieve out-paces a silo’s ability to turn around requests for services. The silo then becomes a bottleneck rather than an IT governance mechanism. When we take an economic view, the value of the silo becomes questionable.
Improving delivery effectiveness
At each level of abstraction, we are using automated software testing tools to support emergent design. We have to use these tools appropriately; and that means understanding a bit about software testing. That doesn’t mean TDD changes software design into a testing activity; it means that with contemporary development methods we are conflating two subdisciplines of software development – programming and testing.
From a process perspective, combining programming and testing activities eliminates one step from the software delivery process. That translates into shorter lead times for software products. Delays as programmers wait for a tester to become available are eliminated, and repeated round-trips between testing and programming specialists are eliminated.
From a communication perspective, combining programming and testing acivities eliminates one communication boundary between development specialists. That translates into fewer misunderstandings and greater clarity about stakeholder needs. Teams work together as a group (Mob Programming), programmers and testers work together directly in real time with no hand-offs (Pairing), or individuals cultivate both programming and testing skills in themselves.
From a technical perspective, combining programming and testing activities means programmers must learn software testing skills. A unit test suite that doesn’t cover all the necessary cases, or that yields false positives, or that won’t run reliably because of external dependencies, will provide little or no value.
Does TDD produce bad designs?
Some detractors say that using TDD leads to poor designs. A countersuggestion is that poor design leads to poor designs, and the tools or methods used to realize the poor design can’t reasonably be blamed for undesirable outcomes.
It’s very common for professional software developers to create modules that have multiple responsibilities, excessive conditional complexity, and a host of other issues. Unless and until they learn to apply widely-accepted software engineering practices and sound software design principles, they will continue to produce bad designs no matter what tools and techniques they use.
Dave, there are broken links in the message 🙂 Didn’t you build it test-first? 😉 Grazie
Guilty as charged! (Or I could pretend I did that to illustrate the value of the test-first approach. Yeah, let’s say that. It sounds better.)
Very well written.