- •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
}
TEST(ArabicToRomanNumeralsConverterTestCase, 10_isConvertedTo_X) { ASSERT_EQ("X", convertArabicNumberToRomanNumeral(10));
}
TEST(ArabicToRomanNumeralsConverterTestCase, 20_isConvertedTo_XX) { ASSERT_EQ("XX", convertArabicNumberToRomanNumeral(20));
}
TEST(ArabicToRomanNumeralsConverterTestCase, 30_isConvertedTo_XXX) { ASSERT_EQ("XXX", convertArabicNumberToRomanNumeral(30));
}
Remember what I wrote about test code quality in Chapter 2: the quality of the test code must be as high as the quality of the production code. In other words, our tests need to be refactored, because they contain a lot of duplication and should be designed more elegantly. Furthermore, we want to increase their readability and maintainability. But what can we do?
Take a look at the six tests. The verification in the tests is always the same and could be read more generally as: “Check if Arabic number <x> is converted to the Roman numeral <string>.”
A solution could be to provide a dedicated assertion (also known as custom assertion or custom matcher) for that purpose, which can be read in the same way:
checkIf(x).isConvertedToRomanNumeral("string");
More Sophisticated Tests with a Custom Assertion
To implement our custom assertion, we first of all write a unit test that fails, but different from the unit tests we’ve written before:
TEST(ArabicToRomanNumeralsConverterTestCase, 33_isConvertedTo_XXXIII) { checkIf(33).isConvertedToRomanNumeral("XXXII");
}
354
Chapter 8 Test-Driven Development
The probability is very high that the conversion of 33 already works. Therefore, we force the test to fail (RED) by specifying an intentionally wrong result as the expected value (“XXXII”). But this new test also fails due to another reason: the compiler cannot compile the unit test without errors. A function named checkIf() does not exist yet, equally there is no isConvertedToRomanNumeral().
So, we must first satisfy the compiler by writing the custom assertion. This will consist of two parts (Listing 8-15):
•\ |
A free checkIf(<parameter>) function, returning one instance of a |
|
custom assertion class. |
•\ |
The custom assertion class that contains the real assertion method, |
|
verifying one or various properties of the tested object. |
Listing 8-15. A Custom Assertion for Roman Numerals
class RomanNumeralAssert { public:
RomanNumeralAssert() = delete;
explicit RomanNumeralAssert(const unsigned int arabicNumber) : arabicNumberToConvert(arabicNumber) { }
void isConvertedToRomanNumeral(std::string_view expectedRomanNumeral) const {
ASSERT_EQ(expectedRomanNumeral, convertArabicNumberToRomanNumeral(arabicNumberToConvert));
}
private:
const unsigned int arabicNumberToConvert;
};
RomanNumeralAssert checkIf(const unsigned int arabicNumber) { RomanNumeralAssert assert { arabicNumber };
return assert;
}
355
Chapter 8 Test-Driven Development
Note Instead of a free function checkIf(), a static and public class method can also be used in the assertion class. This can be necessary when you're facing ODR violations, for example, clashes of identical function names. Of course, then the namespace name must be prepended when using the class method:
RomanNumeralAssert::checkIf(33).isConvertedToRomanNumeral ("XXXIII");
Now the code can be compiled without errors, but the new test will fail as expected during execution. See Listing 8-16.
Listing 8-16. An Excerpt from the Output of Google Test on stdout
[ RUN ] ArabicToRomanNumeralsConverterTestCase.33_isConvertedTo_XXXIII
../ArabicToRomanNumeralsConverterTestCase.cpp:30: Failure
Value of: convertArabicNumberToRomanNumeral(arabicNumberToConvert) Actual: "XXXIII"
Expected: expectedRomanNumeral Which is: "XXXII"
[ FAILED ] ArabicToRomanNumeralsConverterTestCase.33_isConvertedTo_XXXIII (0 ms)
So, we need to modify the test and correct the Roman numeral that we expect as the result. See Listing 8-17.
Listing 8-17. Our Custom Asserter Allows a More Compact Spelling of the Test Code
TEST(ArabicToRomanNumeralsConverterTestCase, 33_isConvertedTo_XXXIII) { checkIf(33).isConvertedToRomanNumeral("XXXIII");
}
Now we can sum up all previous tests into a single one, as shown in Listing 8-18.
Listing 8-18. All Checks Can Be Elegantly Pooled Into One Test Function
TEST(ArabicToRomanNumeralsConverterTestCase, conversionOfArabicNumbersToRomanNumerals_Works) {
356
Chapter 8 Test-Driven Development
checkIf(1).isConvertedToRomanNumeral("I"); checkIf(2).isConvertedToRomanNumeral("II"); checkIf(3).isConvertedToRomanNumeral("III"); checkIf(10).isConvertedToRomanNumeral("X"); checkIf(20).isConvertedToRomanNumeral("XX"); checkIf(30).isConvertedToRomanNumeral("XXX"); checkIf(33).isConvertedToRomanNumeral("XXXIII");
}
Take a look at our test code now: redundancy-free, clean, and easily readable. The directness of our self-made assertion is quite elegant. And it is blindingly easy to add more tests now, because we have just to write a single line of code for every new test.
You might complain that this refactoring also has a small disadvantage. The name of the test method is now less specific than the name of all test methods prior to the refactoring (see the section on unit test names in Chapter 2). Can we tolerate these small drawbacks? I think yes. We’ve made a compromise here: This little disadvantage is compensated by the benefits in terms of maintainability and extensibility of our tests.
Now we can continue the TDD cycle and implement the production code successively for the following three tests:
checkIf(100).isConvertedToRomanNumeral("C"); checkIf(200).isConvertedToRomanNumeral("CC"); checkIf(300).isConvertedToRomanNumeral("CCC");
After three iterations, the code will look like Listing 8-19 prior to the refactoring step.
Listing 8-19. Our Conversion Function in the Ninth TDD Cycle Before Refactoring
std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { std::string romanNumeral;
if (arabicNumber == 100) { romanNumeral = "C";
}else if (arabicNumber == 200) { romanNumeral = "CC";
}else if (arabicNumber == 300) { romanNumeral = "CCC";
}else {
357
Chapter 8 Test-Driven Development
while (arabicNumber >= 10) { romanNumeral += "X"; arabicNumber -= 10;
}
while (arabicNumber >= 1) { romanNumeral += "I"; arabicNumber--;
}
}
return romanNumeral;
}
And again, the same pattern emerges as before with 1, 2, 3; and 10, 20, and 30. We can also use a similar loop for the hundreds, as shown in Listing 8-20.
Listing 8-20. The Emerging Pattern, as Well as Which Parts of the Code Are Variable and Which Are Identical, Is Clearly Recognizable
std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { std::string romanNumeral;
while (arabicNumber >= 100) { romanNumeral += "C"; arabicNumber -= 100;
}
while (arabicNumber >= 10) { romanNumeral += "X"; arabicNumber -= 10;
}
while (arabicNumber >= 1) { romanNumeral += "I"; arabicNumber--;
}
return romanNumeral;
}
358