Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt.pdf
Скачиваний:
29
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 8 Test-Driven Development

Test-driven development can be a driver and enabler for a good and sustainable software design. As with many other tools and methods, the practice of TDD cannot guarantee a good design. It is not a silver bullet for design issues. The design decisions are still taken by the developer and not by the tool. At the least, TDD is a helpful approach to avoid what might be perceived as bad design. Many developers who use TDD in their daily work can confirm that it is extremely hard to produce or tolerate bad and messy code with this approach.

And there is no doubt about when a developer has finished implementing all required functionalities: if all unit tests are green and they cannot find any more new test cases that would lead to new code, it means that all requirements on the unit are satisfied and the job is done! And an enjoyable side effect is that it’s done in high quality.

In addition, the TDD workflow also drives the design of the unit to be developed, especially its interface. With TDD and test first, the API’s design and implementation are guided by its test cases. Anyone who has ever tried to retrofit legacy code with unit tests knows how difficult that could be. These systems are typically built “code first.” Many inconvenient dependencies and a bad API design complicate testing in such systems. And if a software unit is hard to test, it is also hard to (re-)use. In other words, TDD gives early feedback on a software unit’s usability, that is, how simple that piece of software can be integrated and used in its planned execution environment.

When We Should Not Use TDD

The final question is this: should we develop every piece of code using a test-first approach?

My clear answer is probably not!

No doubt, test-driven development is a great practice to guide the design and implementation of a piece of software. Theoretically, it would even be possible to develop almost all parts of a software system this way. And as a kind of positive side effect, the emerging code is 100% tested by default.

But some parts of a project are so simple, small, or less complex, that they don’t justify this approach. If you can write your code quickly off the cuff, because complexity and risks are low, then of course you can do that. Examples of such situations are pure data classes without functionality (which could be, by the way, a smell, but for other reasons; see the section about anemic classes in Chapter 6), or simple glue code that couples together two modules.

369

Chapter 8 Test-Driven Development

Furthermore, prototyping can be a very difficult task with TDD. When you enter new territory, or you should develop software in a very innovative environment without domain experience, you’re sometimes not sure what road you’re going to take to a solution. Writing unit tests first in projects with very volatile and fuzzy requirements can be an extremely challenging task. Sometimes it’s better to write down a rudimentary solution easily and quickly then, and to ensure its quality in a later step with the help of retrofitted unit tests.

Another big challenge, for which TDD won’t help, is getting a good architecture. TDD does not replace the necessary reflecting on the coarse-grained structures (subsystems, components, etc.) of your software system. When you are faced with fundamental decisions about frameworks, libraries, technologies, or architecture patterns, TDD might not be the appropriate approach to make them.

UI code also seems to resists this practice. It seems difficult to impossible to develop the (graphical) user interface of an application in a test-driven way. Among other things, this may be due to the fact that it takes a certain amount of imagination to get an idea

of what the UI should look like: How should the user be guided visually through the flow of an use case? How many screens or dialogs are we talking about? What are the preconditions? What does visual feedback to the user look like in the event of an error? All of these questions may not be easily answered, even by domain experts and other stakeholders.

In such cases, a method called behavior driven development (BDD) might be helpful, an extension of TDD. BDD promotes writing specification stories with acceptance criteria together with the people from business and QA. These stories can be—virtually or real—executed step by step, stimulating the software to be developed to change its state. With BDD, the user's interaction with the software can be systematically explored and requirements for the UI can be derived.

For anything else, I strongly recommend TDD. This approach can save a lot of time, headaches, and false starts when you must develop a software unit, like a function or class, in C++.

“For anything that is more complex than just a few lines of code, software craftsmen can test-drive code as fast as other developers can write code without tests, if not faster.”

—Sandro Mancuso

370

Chapter 8 Test-Driven Development

Tip  If you want to dive deeper into test-driven development with C++, I recommend the excellent book Modern C++ Programming with Test-Driven Development [Langr13] by Jeff Langr. Jeff’s book offers much deeper insights into TDD and gives you hands-on lessons about the challenges and rewards of doing TDD in C++.

TDD Is Not a Replacement for Code Reviews

“Given enough eyeballs, all bugs are shallow.”

—Eric S. Raymond, Linus’s law, The Cathedral and the Bazaar, 1999

Let’s conclude this chapter with a topic that plays a major role not only in the environment of C++ development projects: code reviews.

The so-called multi-eye principle applied in code reviews may seem somewhat old-­fashioned and time-consuming in today’s world, but it is a proven means of knowledge sharing and quality assurance in software development. Who hasn’t had this experience—that sometimes it’s enough just to have a colleague take a quick look at a tricky problem you’ve been brooding over for hours and she can immediately give you the decisive tip to solve it?

A code review is much more than just reviewing and improving a piece of code by another developer. In a team, code reviews also help developers get to know their code base better. They can share ideas about how to implement a piece of software better, and they also learn new technologies and practices that enhance their skills. A code review simplifies conversations about the code base, making it easier and faster for new team members to understand the system being developed.

The following questions and points can be clarified and discussed in a code review:

•\

Are there any obvious bugs in the reviewed code?

•\

Does the reviewed code meet the requirements for readable,

 

understandable and well-maintainable code? Were clean code

 

principles followed? Are there any bad and unwanted dependencies?

371

Chapter 8 Test-Driven Development

•\

Have all requirements for the reviewed piece of code been

 

considered and implemented?

•\

Are the tests sufficient for the reviewed piece of code?

•\

Does new code conform to existing style and formatting guidelines?

•\

Is the implementation of the reviewed piece of code appropriate

 

and efficient, or are there shorter, more elegant, and better ways to

 

achieve the same result (e.g., a library function that the developers

 

didn’t know about)?

•\

Is there anything I can learn, take, or draw a lesson from the

 

developers of the piece of code being reviewed, e.g., a particularly

 

elegant and good solution, or a library feature that was previously

 

unknown to me?

Code reviews can be performed in a variety of ways. An informal code review can be that you simply ask your colleagues to take a quick look at the code you just wrote and to see if they notice anything (“over-the-shoulder review”). In some development organizations, reviews are formalized and there is even a defined process for them. In

some other organizations, code reviews are organized as a regular and social team event, e.g., once a week the development team meets for one or two hours over coffee and cake to review and discuss some code. With pair programming, a continuous code review takes place, so to speak, because the multiple-eyes principle is already in effect while the code is written.

In addition to physical (peer) code review techniques, where a few team members get together for an hour or two, there are also centralized software tools with code review capabilities, often integrated with the version control system and the developers IDEs. These have advantages as well as disadvantages. Advantages are that such tools automatically­ ensure traceability, and they often provide reporting as well. In addition, they also seem to save time, since a time-consuming meeting for reviewing a piece of code is not required.

The downside of these tools is that you have to find one that fits well into your development process. Moreover, it should be ensured that such tools in no way replace the indispensable face-to-face communication of the team: software development is also a social activity, and direct communication is a crucial factor for success.

372

Chapter 8 Test-Driven Development

Some people say that with test-driven development, you can largely do without code reviews. They argue that with the help of TDD, developers are enforced to produce code of such high quality that a review then no longer brings any further improvements.

But beware: This is a fallacy! If each developer writes test cases, code, and APIs for himself alone, the view of another developer is missing, i.e., someone who can check the aforementioned points from a different perspective. Every developer would stew in her own juice. Thus, no knowledge sharing can take place, nor can people learn from each other. In the medium to long term, code quality will suffer.

One way to combine code reviews and TDD and perform both in parallel is the aforementioned pair programming. The pairing partners can discuss test cases, algorithms, structure, naming, and API design and thus improve together. Bugs can be spotted much earlier, because four eyes see more than two. Early and frequent feedback can raise the code quality significantly, and you can’t get an earlier feedback than from your pairing partner. And by changing pairings, knowledge is continuously distributed throughout the team.

373

CHAPTER 9

Design Patterns

and Idioms

Good craftspeople can draw on a wealth of experience and knowledge. Once they’ve found a good solution for a certain problem, they take this solution into their repertoire to apply it in the future to a similar problem. Ideally, they transform their solution into something that is known as a canonical form and document it, both for themselves and for others.

CANONICAL FORM

The term canonical form in this context describes a representation of something that is reduced to its simplest and most significant form without losing generality. Related to design patterns, the canonical form of a pattern describes its most basic elements: name, context, problem, forces, solution, examples, drawbacks, etc.

This is also true for software developers. Experienced developers can draw on a wealth of sample solutions for constantly recurring design problems in software. They share their knowledge with others and make it reusable for similar problems. The principle behind this is don’t reinvent the wheel!

In 1995, a much-noticed and famous book was published. Some people even say that it was one of the most important books that has ever been written in software history. The book’s title is Design Patterns: Elements of Reusable Object-Oriented Software

[Gamma95]. Its four authors, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, also known as the Gang of Four (GoF), introduced the concept of design patterns into software development and presented a catalogue of 23 object-oriented design patterns.

375

© Stephan Roth 2021

S. Roth, Clean C++20, https://doi.org/10.1007/978-1-4842-5949-8_9