![](/user_photo/_userpic.png)
- •Table of Contents
- •About the Author
- •About the Technical Reviewer
- •Acknowledgments
- •Software Entropy
- •Clean Code
- •C++11: The Beginning of a New Era
- •Who This Book Is For
- •Conventions Used in This Book
- •Sidebars
- •Notes, Tips, and Warnings
- •Code Samples
- •Coding Style
- •C++ Core Guidelines
- •Companion Website and Source Code Repository
- •UML Diagrams
- •The Need for Testing
- •Unit Tests
- •What About QA?
- •Rules for Good Unit Tests
- •Test Code Quality
- •Unit Test Naming
- •Unit Test Independence
- •One Assertion per Test
- •Independent Initialization of Unit Test Environments
- •Exclude Getters and Setters
- •Exclude Third-Party Code
- •Exclude External Systems
- •What Do We Do with the Database?
- •Don’t Mix Test Code with Production Code
- •Tests Must Run Fast
- •How Do You Find a Test’s Input Data?
- •Equivalence Partitioning
- •Boundary Value Analysis
- •Test Doubles (Fake Objects)
- •What Is a Principle?
- •KISS
- •YAGNI
- •It’s About Knowledge!
- •Building Abstractions Is Sometimes Hard
- •Information Hiding
- •Strong Cohesion
- •Loose Coupling
- •Be Careful with Optimizations
- •Principle of Least Astonishment (PLA)
- •The Boy Scout Rule
- •Collective Code Ownership
- •Good Names
- •Names Should Be Self-Explanatory
- •Use Names from the Domain
- •Choose Names at an Appropriate Level of Abstraction
- •Avoid Redundancy When Choosing a Name
- •Avoid Cryptic Abbreviations
- •Avoid Hungarian Notation and Prefixes
- •Avoid Using the Same Name for Different Purposes
- •Comments
- •Let the Code Tell the Story
- •Do Not Comment Obvious Things
- •Don’t Disable Code with Comments
- •Don’t Write Block Comments
- •Don’t Use Comments to Substitute Version Control
- •The Rare Cases Where Comments Are Useful
- •Documentation Generation from Source Code
- •Functions
- •One Thing, No More!
- •Let Them Be Small
- •“But the Call Time Overhead!”
- •Function Naming
- •Use Intention-Revealing Names
- •Parameters and Return Values
- •Avoid Flag Parameters
- •Avoid Output Parameters
- •Don’t Pass or Return 0 (NULL, nullptr)
- •Strategies for Avoiding Regular Pointers
- •Choose simple object construction on the stack instead of on the heap
- •In a function’s argument list, use (const) references instead of pointers
- •If it is inevitable to deal with a pointer to a resource, use a smart one
- •If an API returns a raw pointer...
- •The Power of const Correctness
- •About Old C-Style in C++ Projects
- •Choose C++ Strings and Streams over Old C-Style char*
- •Use C++ Casts Instead of Old C-Style Casts
- •Avoid Macros
- •Managing Resources
- •Resource Acquisition Is Initialization (RAII)
- •Smart Pointers
- •Unique Ownership with std::unique_ptr<T>
- •Shared Ownership with std::shared_ptr<T>
- •No Ownership, but Secure Access with std::weak_ptr<T>
- •Atomic Smart Pointers
- •Avoid Explicit New and Delete
- •Managing Proprietary Resources
- •We Like to Move It
- •What Are Move Semantics?
- •The Matter with Those lvalues and rvalues
- •rvalue References
- •Don’t Enforce Move Everywhere
- •The Rule of Zero
- •The Compiler Is Your Colleague
- •Automatic Type Deduction
- •Computations During Compile Time
- •Variable Templates
- •Don’t Allow Undefined Behavior
- •Type-Rich Programming
- •Know Your Libraries
- •Take Advantage of <algorithm>
- •Easier Parallelization of Algorithms Since C++17
- •Sorting and Output of a Container
- •More Convenience with Ranges
- •Non-Owning Ranges with Views
- •Comparing Two Sequences
- •Take Advantage of Boost
- •More Libraries That You Should Know About
- •Proper Exception and Error Handling
- •Prevention Is Better Than Aftercare
- •No Exception Safety
- •Basic Exception Safety
- •Strong Exception Safety
- •The No-Throw Guarantee
- •An Exception Is an Exception, Literally!
- •If You Can’t Recover, Get Out Quickly
- •Define User-Specific Exception Types
- •Throw by Value, Catch by const Reference
- •Pay Attention to the Correct Order of Catch Clauses
- •Interface Design
- •Attributes
- •noreturn (since C++11)
- •deprecated (since C++14)
- •nodiscard (since C++17)
- •maybe_unused (since C++17)
- •Concepts: Requirements for Template Arguments
- •The Basics of Modularization
- •Criteria for Finding Modules
- •Focus on the Domain of Your Software
- •Abstraction
- •Choose a Hierarchical Decomposition
- •Single Responsibility Principle (SRP)
- •Single Level of Abstraction (SLA)
- •The Whole Enchilada
- •Object-Orientation
- •Object-Oriented Thinking
- •Principles for Good Class Design
- •Keep Classes Small
- •Open-Closed Principle (OCP)
- •A Short Comparison of Type Erasure Techniques
- •Liskov Substitution Principle (LSP)
- •The Square-Rectangle Dilemma
- •Favor Composition over Inheritance
- •Interface Segregation Principle (ISP)
- •Acyclic Dependency Principle
- •Dependency Inversion Principle (DIP)
- •Don’t Talk to Strangers (The Law of Demeter)
- •Avoid Anemic Classes
- •Tell, Don’t Ask!
- •Avoid Static Class Members
- •Modules
- •The Drawbacks of #include
- •Three Options for Using Modules
- •Include Translation
- •Header Importation
- •Module Importation
- •Separating Interface and Implementation
- •The Impact of Modules
- •What Is Functional Programming?
- •What Is a Function?
- •Pure vs Impure Functions
- •Functional Programming in Modern C++
- •Functional Programming with C++ Templates
- •Function-Like Objects (Functors)
- •Generator
- •Unary Function
- •Predicate
- •Binary Functors
- •Binders and Function Wrappers
- •Lambda Expressions
- •Generic Lambda Expressions (C++14)
- •Lambda Templates (C++20)
- •Higher-Order Functions
- •Map, Filter, and Reduce
- •Filter
- •Reduce (Fold)
- •Fold Expressions in C++17
- •Pipelining with Range Adaptors (C++20)
- •Clean Code in Functional Programming
- •The Drawbacks of Plain Old Unit Testing (POUT)
- •Test-Driven Development as a Game Changer
- •The Workflow of TDD
- •TDD by Example: The Roman Numerals Code Kata
- •Preparations
- •The First Test
- •The Second Test
- •The Third Test and the Tidying Afterward
- •More Sophisticated Tests with a Custom Assertion
- •It’s Time to Clean Up Again
- •Approaching the Finish Line
- •Done!
- •The Advantages of TDD
- •When We Should Not Use TDD
- •TDD Is Not a Replacement for Code Reviews
- •Design Principles vs Design Patterns
- •Some Patterns and When to Use Them
- •Dependency Injection (DI)
- •The Singleton Anti-Pattern
- •Dependency Injection to the Rescue
- •Adapter
- •Strategy
- •Command
- •Command Processor
- •Composite
- •Observer
- •Factories
- •Simple Factory
- •Facade
- •The Money Class
- •Special Case Object (Null Object)
- •What Is an Idiom?
- •Some Useful C++ Idioms
- •The Power of Immutability
- •Substitution Failure Is Not an Error (SFINAE)
- •The Copy-and-Swap Idiom
- •Pointer to Implementation (PIMPL)
- •Structural Modeling
- •Component
- •Interface
- •Association
- •Generalization
- •Dependency
- •Template and Template Binding
- •Behavioral Modeling
- •Activity Diagram
- •Action
- •Control Flow Edge
- •Other Activity Nodes
- •Sequence Diagram
- •Lifeline
- •Message
- •State Diagram
- •State
- •Transitions
- •External Transitions
- •Internal Transitions
- •Trigger
- •Stereotypes
- •Bibliography
- •Index
Chapter 8 Test-Driven Development
been built previously. And in some development organizations, this approach is even mistakenly named as “test-driven development,” which is flat wrong.
Like I said, plain old unit testing is better than no unit testing at all. Nonetheless, this approach has a few disadvantages:
•\ |
There is no compulsion to write the unit tests afterward. Once a |
|
feature works (…or rather seems to work), there is little motivation |
|
to retrofit the code with unit tests. It’s no fun, and the temptation |
|
to move on to the next exciting task is just too great for many |
|
developers. |
•\ |
The resulting code can be difficult to test. Often it is not so easy to |
|
retrofit existing code with unit tests, because the initial developers |
|
didn’t set great store by its testability. This tends to favor the |
|
emergence of tightly coupled code. |
•\ |
It is not easy to reach pretty-high test coverage with retrofitted unit |
|
tests. The writing of unit tests after the code has the tendency that |
|
some issues or bugs can slip through. |
Test-Driven Development as a Game Changer
Test-driven development (TDD) turns traditional development completely around. For developers who have not yet dealt with TDD, this approach represents a paradigm shift.
As a so-called test-first approach and in contrast to POUT, TDD does not allow any production code to be written before the associated test has been written that justifies that code. In other words, TDD means that we write the test for a new feature or function always before we write the corresponding production code. This is done strictly step by step: after each implemented test, just enough production code is written that the test will pass and no more! And it is done as long as there are still unrealized requirements for the module to be developed.
At first glance, it seems to be paradoxical and a little bit absurd to write a unit test for something that does not yet exist. How can this work?
Don’t worry, it works. After we have discussed the process behind TDD in detail in the next section, all doubts will hopefully be eliminated.
338
![](/html/75672/2303/html_3iAzqyDIia.Sav3/htmlconvd-3gbB7Q349x1.jpg)
Chapter 8 Test-Driven Development
The Workflow of TDD
When performing test-driven development, the steps depicted in Figure 8-2 are run through repeatedly until all known requirements for the unit to develop are satisfied.
Figure 8-2. The detailed workflow of TDD as an UML activity diagram
First of all, it is remarkable that the first action after the initial node that is labeled with “Start Doing TDD” is that the developer should think about which requirement to satisfy. Which kinds of requirements are meant here?
Well, first and foremost there are requirements that must be fulfilled by a software system. This applies both to the requirements of the business stakeholders on the top level regarding the whole system, as well as to the requirements residing on lower abstraction levels, that is, requirements for components, classes, and functions, which were derived from the business stakeholders’ requirements. With TDD and its testfirst approach, requirements are nailed down firmly by unit tests. In fact, before the production code is written. In our case of a test-first approach for the development
of units, that is, at the lowest level of the test pyramid (see Figure 2-1 in Chapter 2),
339
Chapter 8 Test-Driven Development
of course the requirements at the lowest level are meant here. Naturally, such a testfirst approach can also be applied at the higher levels of abstraction, such as in an approach named acceptance test–driven development (ATDD), which is a development methodology that encompasses acceptance testing, but claims writing acceptance tests before developers begin coding.
Next, a small test is to be written, whereby the public interface (API) is to be designed. This might be surprising, because in the first run through this cycle, we still have not written any production code. So, what interface can be designed here if we have a blank piece of paper?
Well, the simple answer is this: that “blank piece of paper” is exactly what we want to fill in now, but coming from a different perspective than usual. We take the perspective of a future external client of the piece of software to be developed. We use a small test to define how we want to use the code to be developed. In other words, this is the step that should lead to well-testable and thus also well-usable software units.
After we have written the appropriate lines in the test, we must, of course, also satisfy the compiler and provide the interface requested by the test.
Then immediately the next surprise: the newly written unit test must (initially) fail. Why?
Simple answer: we have to make sure that the test can fail at all. Even a unit test can itself be implemented incorrectly and, for example, always pass, no matter what we’re doing in the production code. So, we have to ensure that the newly written test is armed.
Now we are getting to the climax of this small workflow: we write just enough production code—and not a single line more!—so that the new unit test (… and any previously existing tests) is passed! It is very important to be disciplined at this point and not write more code than required (remember the KISS principle from Chapter 3). It’s up to the developer to decide what is appropriate in each situation. Sometimes a single line of code, or even just one statement, is sufficient; in other cases you need to call a library function. If the latter is the case, the time has now come to think about how to integrate and use this library, and especially how to replace it with a test double (see the section about test doubles in Chapter 2).
If we now run the unit tests and we have done everything right, the tests will pass. We have reached a remarkable point in the process. If the tests pass, we always have
100% unit test coverage at this step. Always! Not only 100% in the sense of a technical test coverage metric, such as condition coverage, branch coverage, or statement coverage. No, much more important is that we have 100% unit test coverage regarding
340
![](/html/75672/2303/html_3iAzqyDIia.Sav3/htmlconvd-3gbB7Q351x1.jpg)
Chapter 8 Test-Driven Development
the requirements that were already implemented at this point! And yes, at this point possibly there may be still some or many non-implemented requirements for the piece of code to be developed. This is okay, because we will go through the TDD cycle again and again until all requirements are satisfied. But for a subset of requirements that are already satisfied at this point, we have 100% unit test coverage.
This fact gives us tremendous power! With this gapless safety net of unit tests, we can now carry out fearless refactorings. Code smells (e.g., duplicated code) or design issues can be fixed. We do not need to be afraid to break functionality, because regularly executed unit tests will give us immediate feedback about that. And the pleasant thing is this: if one or more tests fail during the refactoring phase, the code change that led to it was a very small one.
After the refactoring has been completed, we can implement another requirement that has not yet been fulfilled by continuing the TDD cycle. If there are no more requirements, we are ready.
Figure 8-2 depicts the TDD cycle with many details. Boiled down to its three essential main steps as depicted in Figure 8-3, the TDD cycle is often referred to as “RED – GREEN – REFACTOR.”
Figure 8-3. The core workflow of TDD
341