- •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
These values can also be depicted on a number line, as shown in Figure 2-6.
Figure 2-6. The input parameters derived from a boundary value analysis
If the boundary values are determined and tested for each equivalence partition, then very good test coverage can be achieved in practice with relatively little effort.
Test Doubles (Fake Objects)
Unit tests should only be called “unit tests” if the units to be tested are completely independent from collaborators during test execution, that is, the unit under test does not use other units or external systems. For instance, while the involvement of a
database during an integration test is uncritical and required, because that’s the purpose of an integration test, access (e.g., a query) to this database during a real unit test is proscribed (see the section “What Do We Do with the Database?” earlier in this chapter). Thus, dependencies of the unit to be tested with other modules or external systems should be replaced with so-called test doubles, also known as fake objects, or mock-ups.
In order to work in an elegant way with such test doubles, we should strive for loose coupling of the unit under test (see the section entitled “Loose Coupling” in Chapter 3). For instance, an abstraction (e.g., an interface in the form of a pure abstract class) can be introduced at the access point to an unwanted collaborator, as shown in Figure 2-7.
36
Chapter 2 Build a Safety Net
Figure 2-7. An interface makes it easy to replace the LowLevelModule with a test double
Let’s assume that you want to develop an application that uses an external web service for real-time currency conversions. During a unit test you cannot use this external service naturally, because it delivers different conversion factors every minute. Furthermore, the service is queried via the Internet, which is basically slow and can fail. And it is impossible to simulate borderline cases. Hence, you have to replace the real currency conversion with a test double during the unit test.
First, we have to introduce a variation point in the code to be able to replace the module that communicates with the currency conversion service with a test double. This can be done with the help of an interface, which in C++ is an abstract class with solely pure virtual member functions. See Listing 2-9.
Listing 2-9. An Abstract Interface for Currency Converters
class CurrencyConverter { public:
virtual ~CurrencyConverter() { }
virtual long double getConversionFactor() const = 0;
};
The access to the currency conversion service via the Internet is encapsulated in a class that implements the CurrencyConverter interface. See Listing 2-10.
37
Chapter 2 Build a Safety Net
Listing 2-10. The Class that Accesses the Realtime Currency Conversion Service
class RealtimeCurrencyConversionService : public CurrencyConverter { public:
virtual long double getConversionFactor() const override;
// ...more members here that are required to access the service...
};
For testing purposes, a second implementation exists: The CurrencyConversionServiceMock test double. Objects of this class will return a defined and predictable conversion factor as it is required for unit testing. Furthermore, objects of this class provide the capability to set the conversion factor from the outside, for example, to simulate borderline cases. See Listing 2-11.
Listing 2-11. The Test Double
class CurrencyConversionServiceMock : public CurrencyConverter { public:
virtual long double getConversionFactor() const override { return conversionFactor;
}
void setConversionFactor(const long double value) { conversionFactor = value;
}
private:
long double conversionFactor{0.5};
};
At the place in the production code where the currency converter is used, the interface is now used to access the service. Due to this abstraction, it is totally
transparent to the client’s code which kind of implementation is used during runtime— either the real currency converter or its test double. See Listings 2-12 and 2-13.
Listing 2-12. The Header of the Class that Uses the Service
#include <memory>
class CurrencyConverter;
38
Chapter 2 Build a Safety Net
class UserOfConversionService { public:
UserOfConversionService() = delete;
explicit UserOfConversionService(const std::shared_ ptr<CurrencyConverter>& conversionService);
void doSomething();
// More of the public class interface follows here...
private:
std::shared_ptr<CurrencyConverter> conversionService_; //...internal implementation...
};
Listing 2-13. An Excerpt from the Implementation File
UserOfConversionService::UserOfConversionService (const std::shared_ ptr<CurrencyConverter>& conversionService) :
conversionService_(conversionService) { }
void UserOfConversionService::doSomething() {
long double conversionFactor = conversionService_->getConversionFactor(); // ...
}
In a unit test for the UserOfConversionService class, the test case can now pass in the mock object through the initialization constructor. On the other hand, during normal operations, the real service can be passed through the constructor. This technique
is a design pattern called dependency injection, which is discussed in detail in the eponymous section of Chapter 9. See Listing 2-14.
Listing 2-14. UserOfConversionService Gets its Required CurrencyConverter Object
auto serviceToUse =
std::make_shared</* name of the desired class here */>(); UserOfConversionService user(serviceToUse);
// The instance of UserOfConversionService is ready for use...
user.doSomething();
39