- •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 2 Build a Safety Net
Listing 2-4. Question: Is It a Good Idea to Check All Comparison Operators in One Unit Test?
void MoneyTest::givenTwoMoneyObjectsWithDifferentBalance_ testAllComparisonOperators() {
const Money m1(-4000.0); const Money m2(2000.0); ASSERT_TRUE(m1 != m2); ASSERT_FALSE(m1 == m2); ASSERT_TRUE(m1 < m2); ASSERT_FALSE(m1 > m2);
// ...more assertions here...
}
I think the problems with this approach are obvious:
•\ |
If a test can fail for several reasons, it can be difficult for developers |
|
to find the cause of the error quickly. Above all, an early assertion |
|
that fails obscures additional errors, that is, it hides subsequent |
|
assertions, because the execution of the test is stopped. |
•\ |
As explained in the section “Unit Test Naming,” we should name a |
|
test in a precise and expressive way. With multiple assertions, a unit |
|
test tests many things (which is, by the way, a violation of the single |
|
responsibility principle; see Chapter 6), and it would be difficult to |
|
find a good name for it. The ...testAllComparisonOperators() |
|
name is not precise enough. |
Independent Initialization of Unit Test Environments
This rule is somewhat akin to unit test independence. When a cleanly implemented test completes, all states related to that test must disappear. In more specific terms, when running all unit tests, each test must be an isolated partial instantiation of an
application. Each test has to set up and initialize its required environment completely on its own. The same applies to cleaning up after the execution of the test.
26
Chapter 2 Build a Safety Net
Exclude Getters and Setters
Don’t write unit tests for usual getters and setters of a class, as shown in Listing 2-5.
Listing 2-5. A Simple Setter and Getter
void Customer::setForename(const std::string& forename) { this->forename = forename;
}
const std::string& Customer::getForename() const { return forename;
}
Do you really expect that something could go wrong with such straightforward methods? These member functions are typically so simple that it would be foolish to write unit tests for them. Furthermore, usual getters and setters are implicitly tested by other and more important unit tests.
Attention, I just wrote that it is not necessary to test usual and simple getters and setters. Sometimes, getters and setters are not that simple. According to the information hiding principle (see the section about information hiding in Chapter 3) that we will discuss later, it should be hidden from the client if a getter is simple and stupid, or if it has to make complex things to determine its return value. Therefore, it can sometimes be useful to write an explicit test for a getter or setter.
Exclude Third-Party Code
Don’t write tests for third-party code! We don’t have to verify that libraries or frameworks do work as expected. For example, we can assume with a clear conscience that the used member function std::vector::push_back() from the C++ Standard Library works correctly. On the contrary, we can expect that third-party code comes with its own unit tests. It can be a wise architectural decision to not use libraries or frameworks in your project that don’t have their own unit tests and whose quality is doubtful.
27
Chapter 2 Build a Safety Net
Exclude External Systems
The same is true for external systems. Don’t write tests for external systems that are part of the context of your system to be developed, and thus are not in your responsibility. For instance, if your financial software uses an existing, external currency conversion system that is connected via the Internet, you should not test this. Besides the fact that such
a system cannot provide a defined answer (the conversion factor between currencies varies minute by minute), and that such a system might be impossible to reach due to network issues, we are not responsible for the external system.
My advice is to mock (see the section “Test Doubles (Fake Objects)” later in this chapter) these things out and to test your code, not theirs.
What Do We Do with the Database?
Many IT systems contain (relational) databases nowadays. They are required to persist huge amounts of objects or data into longer-term storage, so that these objects or data can be queried in a comfortable way and survive a system shutdown.
An important question is this: what do you do with the database during unit testing?
“My first and overriding piece of advice on this subject is: When there is any way to test without a database, test without the database!”
—Gerard Meszaros, xUnit Test Patterns
Databases can cause diverse and sometimes subtle problems during unit testing. For instance, if many unit tests use the same database, the database tends to become a large central storage that those tests must share for different purposes. This sharing may adversely affect the independence of the unit tests I discussed earlier in this chapter. It could be difficult to guarantee the required precondition for each unit test. The execution of a single unit test can cause unwanted side effects for other tests via the commonly used database.
Another problem is that databases are basically slow. They are much slower than access to local computer memory. Unit tests that interact with the database tend to run magnitudes slower than tests that can run entirely in memory. Imagine you have a few hundred unit tests, and each test needs an extra time span of 500ms on average, caused by the database queries. In sum, all the tests take several minutes longer than without a database.
My advice is to mock out the database (see the section about test doubles/mock objects later in this chapter) and execute all the unit tests solely in memory. Don’t worry: the database, if it exists, will be involved at the integration and system testing level.
28
Chapter 2 Build a Safety Net
Don’t Mix Test Code with Production Code
Sometimes developers come up with the idea to equip their production code with test code. For example, a class might contain code to handle a dependency to a collaborating class during a test in the manner shown in Listing 2-6.
Listing 2-6. One Possible Solution to Deal with a Dependency During Testing
#include <memory>
#include "DataAccessObject.h" #include "CustomerDAO.h" #include "FakeDAOForTest.h"
using DataAccessObjectPtr = std::unique_ptr<DataAccessObject>;
class Customer { public:
Customer() = default;
explicit Customer(const bool testMode) : inTestMode(testMode) {}
void save() {
DataAccessObjectPtr dataAccessObject = getDataAccessObject(); // ...use dataAccessObject to save this customer...
}
// ...
private:
DataAccessObjectPtr getDataAccessObject() const { if (inTestMode) {
return std::make_unique<FakeDAOForTest>();
}else {
return std::make_unique<CustomerDAO>();
}
}
// ...more operations here...
bool inTestMode{ false };
// ...more attributes here...
};
29
Chapter 2 Build a Safety Net
DataAccessObject is the abstract base class of specific DAOs, in this case, CustomerDAO and FakeDAOForTest. The last one is a so-called fake object, which is simply a test double (see the section about test doubles later in this chapter). It is intended to replace the real DAO, since we do not want to test it, and we don’t want to save the customer during the test (remember my advice about databases). The Boolean data member inTestMode determines which one of the DAOs is used.
Well, this code would work, but the solution has several disadvantages.
First of all, the production code is cluttered with test code. Although it does not appear dramatically at first sight, it can increase complexity and reduce readability. We need an additional member to distinguish between the test mode and production usage of our system. This Boolean member has nothing to do with a customer, not to mention with our system’s domain. And it’s easy to imagine that this kind of member is required in many classes in our system.
Moreover, the Customer class has dependencies to CustomerDAO and FakeDAOForTest. You can see it in the list of includes at the top of the source code. This means that the test dummy FakeDAOForTest is also part of the system in the production environment. It is to be hoped that the code of the test double is never called in production, but it is compiled, linked, and deployed.
Of course, there are more elegant ways to deal with these dependencies and to keep the production code free from test code. For instance, we can inject the specific DAO as a reference parameter in Customer::save(). See Listing 2-7.
Listing 2-7. Avoiding Dependencies to Test Code (1)
class DataAccessObject;
class Customer { public:
void save(DataAccessObject& dataAccessObject) {
// ...use dataAccessObject to save this customer...
}
// ...
};
Alternatively, this can be done while constructing instances of type Customer. In this case, we must hold a reference to the DAO as an attribute of the class. Furthermore, we have to suppress the automatic generation of the default constructor through the
30
Chapter 2 Build a Safety Net
compiler, because we don’t want any user of Customer to be able to create an improperly initialized instance of it. See Listing 2-8.
Listing 2-8. Avoiding Dependencies to Test Code (2)
class DataAccessObject;
class Customer { public:
Customer() = delete;
explicit Customer(DataAccessObject& dataAccessObject) : dataAccessObject_(dataAccessObject) {}
void save() {
// ...use member dataAccessObject to save this customer...
}
//...
private:
DataAccessObject& dataAccessObject_;
//...
};
DELETED FUNCTIONS
In C++, the compiler automatically generates the so-called special member functions (default constructor, copy constructor, copy-assignment operator, and destructor) for a type if it does not declare its own [C++11]. Since C++11, this list of special member functions is extended by the move constructor and move-assignment operator. C++11 (and higher) provides an easy and declarative way to suppress the automatic creation of any special member function, as well as normal member functions and non-member functions: you can delete them. For instance, you can prevent the creation of a default constructor this way:
class Clazz { public:
Clazz() = delete;
};
31