Skip to Content
Course content

130: Unit Testing with Catch2

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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_CASE named "Temperature Conversion" with the tag "[converter]".
  • Implement a SECTION for "Standard Conversions" that verifies 0.0C becomes 32.0F and 100.0C becomes 212.0F.
  • Implement a SECTION for "Absolute Zero" that verifies -273.15C becomes -459.67F.
  • Use REQUIRE for all assertions.

Starter Code:

class TemperatureConverter {
public:
    double celsiusToFahrenheit(double celsius) {
        return (celsius * 9.0 / 5.0) + 32.0;
    }
};
Rating
0 0

There are no comments for now.

to be the first to leave a comment.