C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
230: Test-Driven Development in C++
I've seen this happen a dozen times in professional code reviews. A developer spends three hours crafting a "perfect" piece of logic, feels great about it, and then spends the next two days chasing a ghost in the production logs because they missed one obvious edge case. The problem isn't usually a lack of skill; it's the order of operations.
// The "I'll test it later" approach
class DiscountCalculator {
public:
double applyDiscount(double price, double discountPercentage) {
return price - (price * (discountPercentage / 100.0));
}
};
// Quick manual check in main()
// applyDiscount(100.0, 20.0) -> 80.0. Looks good!
The Silent Failure of Manual Verification
At first glance, the code above is fine. It does exactly what the formula says. But because the developer wrote the implementation first, they only tested the "happy path"βthe scenario where everything goes right. They didn't think about what happens when discountPercentage is 110% or when the price is negative. In a real financial system, that applyDiscount function could accidentally pay a customer to take a product, or create a negative balance that crashes the accounting module.
If we had used Test-Driven Development (TDD), we wouldn't have started with the formula. We would have started by defining the failure.
Breaking the Code Before It Exists
TDD follows a strict rhythm: Red, Green, Refactor. You write a test that fails (Red), you write the minimum code to make it pass (Green), and then you clean up the mess (Refactor). It feels counterintuitive to write a test for a function that doesn't exist yet, but that's exactly where the magic happens. It forces you to act as the client of your own code, thinking about the interface and the edge cases before you get bogged down in the implementation.
Let's flip the script. Instead of writing the logic, we write a test case for that problematic 110% discount scenario using a framework like GoogleTest or Catch2 (I'll use a generic assertion style here for clarity):
// TEST: Should throw an exception if discount is over 100%
void test_DiscountTooHigh() {
DiscountCalculator calc;
try {
calc.applyDiscount(100.0, 110.0);
assert(false); // We expected an exception, but it didn't happen!
} catch (const std::invalid_argument& e) {
// Success: The code correctly rejected the input
}
}
Now, if you run this against our original code, it fails miserably. We are now in the "Red" phase. The failure is our roadmap. It tells us exactly what the code is missing.
Closing the Gap with Minimum Viable Code
The goal of the "Green" phase isn't to write the perfect, final version of the function. It's to make the test pass as quickly as possible. I often see juniors over-engineer this step. Don't. Just solve the problem the test presented.
class DiscountCalculator {
public:
double applyDiscount(double price, double discountPercentage) {
if (discountPercentage > 100.0) {
throw std::invalid_argument("Discount cannot exceed 100%");
}
return price - (price * (discountPercentage / 100.0));
}
};
Now the test passes. We've moved to "Green." Now, and only now, do we look at the code and ask, "Is this the cleanest way to do this?" or "Should discountPercentage be a custom type to prevent this error entirely?" That's the "Refactor" phase. By delaying the cleanup until the tests are green, you ensure that your "improvements" don't accidentally break the logic you just fixed.
Designing for Testability
One thing you'll notice as you dive deeper into TDD is that it reveals "stiff" code. If you find it's impossible to write a test for a function without initializing a database, a network socket, or a 500-line configuration object, your code is too tightly coupled. TDD naturally pushes you toward Dependency Injection. If a class needs a Logger, don't instantiate the logger inside the class; pass it in through the constructor. This allows you to pass a "Mock Logger" during testing, keeping your tests fast and deterministic.
π Practical Task
TDD-driven Overdraft Protection for BankAccount
Your task is to implement a BankAccount class using a TDD approach. You must not write the full implementation of the withdraw method until you have written a failing test for each of the following requirements:
- Requirement 1: Withdrawing an amount less than or equal to the current balance should succeed and decrease the balance.
- Requirement 2: Withdrawing an amount greater than the current balance should throw a
std::runtime_error("Insufficient funds"). - Requirement 3: Attempting to withdraw a negative amount should throw a
std::invalid_argument("Amount must be positive").
Deliverable: Provide the BankAccount class implementation and a set of test functions (or a main function with assertions) that prove all three requirements are met. Ensure that the tests are written in a way that they would have failed if the logic were missing.
There are no comments for now.