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
130: Unit Testing with Catch2
I've seen this happen a dozen times with junior devs: they start writing a new feature and pepper their code with assert() statements. They think, "If the program doesn't crash during my manual run, the code is correct." They've essentially built a rudimentary test suite, but it's a fragile one.
The "Assert" Trap vs. Actual Test Reporting
The problem with assert() is that it's a blunt instrument. The moment an assertion fails, your entire program terminates. If you have ten different edge cases to check and the first one fails, you have no idea if the other nine would have passed. Even worse, in a production Release build, assert() statements are typically stripped out entirely by the preprocessor (via NDEBUG), meaning your "tests" simply vanish when you actually ship the code.
A real unit testing framework like Catch2 doesn't just check if a condition is true; it documents the failure and keeps moving. It tells you exactly what the expression was, what the actual value was, and which specific test case failed, all while allowing the rest of your suite to execute. This transforms testing from a "stop-the-world" event into a diagnostic report.
Defining Behavior with TEST_CASE and REQUIRE
In Catch2, we move away from main() and instead use TEST_CASE macros. Think of a test case as a specification of a feature. Let's look at a simple BankAccount class. I want to ensure that deposits work, but I also want to ensure that negative deposits are ignored.
#include
#include "BankAccount.hpp"
TEST_CASE("BankAccount deposit logic", "[banking]") {
BankAccount account;
// This is our primary assertion
REQUIRE(account.getBalance() == 0.0);
account.deposit(100.0);
REQUIRE(account.getBalance() == 100.0);
// Testing an edge case: negative deposit should be ignored
account.deposit(-50.0);
REQUIRE(account.getBalance() == 100.0);
}
Notice I used REQUIRE. In Catch2, REQUIRE stops the current TEST_CASE if it fails. If you want the test to keep running even after a failure (to see if subsequent checks also fail), you can use CHECK instead. I generally prefer REQUIRE for "critical path" checks—if the balance isn't 0 at the start, there's no point in testing the deposit.
Avoiding Setup Boilerplate with Nested Sections
One of the things I love most about Catch2—and why I prefer it over GoogleTest—is how it handles SECTIONs. In most frameworks, if you want a fresh BankAccount for three different tests, you have to write a "Fixture" class or a complex setup/teardown method. It's tedious and creates a lot of "plumbing" code.
Catch2 handles this by re-running the TEST_CASE from the top for every SECTION. Look at this pattern:
TEST_CASE("BankAccount withdrawals", "[banking]") {
BankAccount account;
account.deposit(100.0); // Shared setup
SECTION("Successful withdrawal") {
bool success = account.withdraw(40.0);
REQUIRE(success == true);
REQUIRE(account.getBalance() == 60.0);
}
SECTION("Overdraft prevention") {
bool success = account.withdraw(150.0);
REQUIRE(success == false);
REQUIRE(account.getBalance() == 100.0);
}
}
Here is the magic: when Catch2 runs this, it executes the TEST_CASE twice. First, it runs the setup and the "Successful withdrawal" section. Then, it starts over, runs the setup again, and executes the "Overdraft prevention" section. You get a fresh account object for every section without having to write a single line of boilerplate setup code. It keeps your tests clean and your intent clear.
📋 Practical Task
Testing the Edge Cases of a Temperature Converter
You are tasked with verifying a TemperatureConverter class. The class has a method double celsiusToFahrenheit(double celsius). You need to ensure the conversion logic is mathematically sound and handles physical limits correctly.
Requirements:
- Create a
TEST_CASEnamed "Temperature Conversion" with the tag "[converter]". - Implement a
SECTIONfor "Standard Conversions" that verifies0.0Cbecomes32.0Fand100.0Cbecomes212.0F. - Implement a
SECTIONfor "Absolute Zero" that verifies-273.15Cbecomes-459.67F. - Use
REQUIREfor all assertions.
Starter Code:
class TemperatureConverter {
public:
double celsiusToFahrenheit(double celsius) {
return (celsius * 9.0 / 5.0) + 32.0;
}
};
There are no comments for now.