- •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
Unit Tests
“‘Refactoring’ without tests isn’t refactoring, it is just moving shit around.”
—Corey Haines (@coreyhaines), December 20, 2013, on Twitter
A unit test is a piece of code that executes a small part of your production code base in a particular context. The test will show you, in a split second, that your code works as you expect it to work. If unit test coverage is pretty high, and you can check in less than a minute that all parts of your system under development are working correctly, this will have numerous advantages:
•\ |
Numerous investigations and studies have proven that fixing bugs |
|
after software is shipped is much more expensive than having unit |
|
tests in place. |
•\ |
Unit tests give you immediate feedback about your entire code |
|
base. Provided that test coverage is sufficiently high (approx. 100%), |
|
developers know in just a few seconds if the code works correctly. |
•\ |
Unit tests give developers the confidence to refactor their code |
|
without fear of doing something wrong that breaks the code. In fact, |
|
a structural change in a code base without a safety net of unit tests is |
|
dangerous and should not be called refactoring. |
•\ |
A high coverage with unit tests can prevent time-consuming and |
|
frustrating debugging sessions. The often hour-long searches for the |
|
causation of a bug using a debugger can be reduced dramatically. Of |
|
course, you will never be able to completely eliminate the use of a |
|
debugger. This tool can still be used to analyze subtle problems, or to |
|
find the cause of a failed unit test. But it will no longer be the pivotal |
|
developer tool to ensure the quality of your code. |
•\ |
Unit tests are a kind of executable documentation because they show |
|
exactly how the code is designed to be used. They are, so to speak, |
|
something of a usage example. |
•\ |
Unit tests can easily detect regressions; that is, they can immediately |
|
show things that used to work, but have unexpectedly stopped |
|
working after a change was made. |
19
Chapter 2 Build a Safety Net
•\ |
Unit testing fosters the creation of clean and well-formed interfaces. |
|
It can help to avoid unwanted dependencies between units. A design |
|
for testability is also a good design for usability; that is, if a piece of |
|
code can easily be mounted against a test fixture, then it can usually |
|
also be integrated with less effort into the system’s production code. |
•\ |
Unit testing makes development go faster. |
The last item in this list appears to be paradoxical and needs a little bit of explanation. Unit testing helps development go faster—how can that be? That doesn’t seem logical.
No doubt about it: writing unit tests takes effort. First and foremost, managers just see that effort and do not understand why developers should invest time into these tests. Especially during the initial phase of a project, the positive effect of unit testing on development speed may not be visible. In these early stages, when the complexity of the system is relatively low and most everything works fine, writing unit tests seems at first just to take effort. But times are changing ...
When the system becomes bigger and bigger (+ 100,000 LOC) and the complexity increases, it becomes more difficult to understand and verify the system (remember software entropy described in Chapter 1). When many developers on different teams are working on a huge system, they are confronted with code written by other developers every day. Without unit tests in place, this can become a very frustrating job. I’m sure everyone knows those stupid, endless debugging sessions, walking through the code in single-step mode while analyzing the values of variables again and again and again. This is a huge waste of time and it will slow down development speed significantly.
Particularly in the mid-to-late stages of development, and in the maintenance phase after product delivery, good unit tests become very valuable. The greatest time savings from unit testing comes a few months or years after a test is written, when a unit or its API needs to be changed or extended.
If test coverage is high, it’s nearly irrelevant whether a piece of code that is edited by a developer was written by himself or by another developer. Good unit tests help developers understand a piece of code written by another person quickly, even if it was written three years ago. If a test fails, it exactly shows where the behavior is broken. Developers can trust that everything still works correctly if all tests pass. Lengthy and annoying debugging sessions become a rarity, and the debugger serves mainly to find the cause of a failed test quickly if this cause is not obvious. And that’s great because it’s fun to work that way. It’s motivating, and it leads to faster and better results. Developers
20
Chapter 2 Build a Safety Net
will have greater confidence in the code base and will feel comfortable with it. Changing requirements or new feature requests? No problem, because they can ship the new product quick and often, and with excellent quality.
UNIT TEST FRAMEWORKS
There are several different unit test frameworks available for C++ development, for example, CppUnit, Boost.Test, CUTE, Google Test, Catch respectively Catch2, and a couple more.
In principle, all these frameworks follow the basic design of so-called xUnit, which is a collective name for several unit test frameworks that derive their structure and functionality from Smalltalk’s SUnit. Apart from the fact that the content of this chapter is not fixated on a specific unit test framework, because its content is applicable to unit testing in general, a full and detailed comparison of all available frameworks is beyond the scope of this book.
Furthermore, choosing a suitable framework is dependent on many factors. For instance, if it is very important to you to be able to add new tests with a minimal amount of work quickly, this might be knock-out criteria for certain frameworks.
What About QA?
A developer might have the following attitude: “Why should I test my software? We have testers and a QA department, it’s their job.”
The essential question is this: Is software quality a sole concern of the quality assurance department?
The simple and clear answer: No!
It would be extremely unprofessional to hand over a piece of software to QA knowing that it contains bugs. Professional developers never foist off the responsibility for a system’s quality on other departments. On the contrary, professional software craftspeople build productive partnerships with the people from QA. They should work closely together and complement each other.
Of course, it is a very ambitious goal to deliver 100% defect-free software. From time to time, QA will find something wrong. And that’s good. QA is our second safety net. They check whether the previous quality assurance measures were sufficient and effective.
21