Table of Contents

  1. Catch2 v3 vs v2
  2. FetchContent Setup
  3. catch_discover_tests()
  4. BDD-Style Tests
  5. Matchers and Generators
  6. Benchmarking Support
  7. Single-Header Fallback
Back to CMake Mastery Series

Catch2

June 4, 2026 Wasil Zafar 8 min read

The complete guide to integrating Catch2 v3 with CMake — FetchContent setup, automatic test discovery, BDD-style tests, matchers, generators, and micro-benchmarking.

Testing

Catch2 v3 vs v2

Catch2 v3 represents a major architectural overhaul from the popular single-header v2. The key differences that affect CMake integration include the transition to a compiled library, improved compile times, and better CTest integration through the official catch_discover_tests() module.

In Catch2 v2, the entire framework was a single header file included in one translation unit with CATCH_CONFIG_MAIN defined. This was simple but caused long compile times. Catch2 v3 ships as a proper static library with separate headers, drastically reducing rebuild times in projects with many test files.

Key Difference: In v2, you linked nothing — just included a header. In v3, you link against Catch2::Catch2WithMain (provides main) or Catch2::Catch2 (bring your own main). The trade-off is faster incremental builds at the cost of one-time library compilation.

Migration from v2 to v3 requires updating includes from #include <catch2/catch.hpp> to specific headers like #include <catch2/catch_test_macros.hpp>. The test macros themselves (TEST_CASE, REQUIRE, CHECK) remain identical.

FetchContent Setup

The recommended approach for integrating Catch2 v3 with CMake uses FetchContent to download and build Catch2 as part of your project:

# CMakeLists.txt — Catch2 v3 via FetchContent
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Build library targets
add_library(mylib src/calculator.cpp src/parser.cpp)
target_include_directories(mylib PUBLIC include)

# Testing setup
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
    include(CTest)
endif()

if(BUILD_TESTING)
    include(FetchContent)
    FetchContent_Declare(
        Catch2
        GIT_REPOSITORY https://github.com/catchorg/Catch2.git
        GIT_TAG        v3.6.0
        GIT_SHALLOW    TRUE
    )
    FetchContent_MakeAvailable(Catch2)

    # Append Catch2's CMake module path for catch_discover_tests
    list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)

    add_subdirectory(tests)
endif()
# tests/CMakeLists.txt — Test executable
add_executable(tests
    test_calculator.cpp
    test_parser.cpp
)
target_link_libraries(tests PRIVATE
    Catch2::Catch2WithMain
    mylib
)

include(Catch)
catch_discover_tests(tests)
Critical Step: You must append ${catch2_SOURCE_DIR}/extras to CMAKE_MODULE_PATH before calling include(Catch). Without this, CMake cannot find the Catch module that provides catch_discover_tests().

catch_discover_tests()

The catch_discover_tests() function registers individual Catch2 test cases as separate CTest tests, enabling granular test selection and parallel execution:

# Full catch_discover_tests configuration
include(Catch)

add_executable(all_tests
    test_math.cpp
    test_strings.cpp
    test_containers.cpp
)
target_link_libraries(all_tests PRIVATE Catch2::Catch2WithMain mylib)

catch_discover_tests(all_tests
    # Prefix test names for filtering
    TEST_PREFIX "unit."
    # Output JUnit XML for CI
    REPORTER junit
    OUTPUT_DIR ${CMAKE_BINARY_DIR}/test-results
    OUTPUT_PREFIX "catch2-"
    OUTPUT_SUFFIX ".xml"
    # Discovery timeout
    DISCOVERY_TIMEOUT 60
    # Additional properties
    PROPERTIES
        LABELS "unit"
        TIMEOUT 30
)
# Run specific tests by name pattern
ctest -R "unit.Calculator"

# Run tests with a specific tag
ctest -R "\[math\]"

# Verbose output on failure
ctest --output-on-failure --parallel 8

Catch2's tag system integrates elegantly with CTest filtering. Each TEST_CASE tag becomes part of the CTest test name, allowing targeted execution:

// test_math.cpp — Tagged test cases
#include <catch2/catch_test_macros.hpp>
#include "calculator.h"

TEST_CASE("Addition works correctly", "[math][addition]") {
    Calculator calc;
    REQUIRE(calc.add(2, 3) == 5);
    REQUIRE(calc.add(-1, 1) == 0);
}

TEST_CASE("Division handles edge cases", "[math][division]") {
    Calculator calc;
    REQUIRE(calc.divide(10, 2) == 5);
    REQUIRE_THROWS_AS(calc.divide(1, 0), std::invalid_argument);
}

BDD-Style Tests

Catch2 supports Behavior-Driven Development syntax with SCENARIO, GIVEN, WHEN, and THEN macros. These generate descriptive test names that integrate naturally with CTest:

// test_account.cpp — BDD-style tests
#include <catch2/catch_test_macros.hpp>
#include "bank_account.h"

SCENARIO("Bank account deposits and withdrawals", "[account][bdd]") {
    GIVEN("An empty bank account") {
        BankAccount account;
        REQUIRE(account.balance() == 0);

        WHEN("A deposit of 100 is made") {
            account.deposit(100);

            THEN("The balance is 100") {
                REQUIRE(account.balance() == 100);
            }

            AND_WHEN("A withdrawal of 40 is made") {
                account.withdraw(40);

                THEN("The balance is 60") {
                    REQUIRE(account.balance() == 60);
                }
            }
        }

        WHEN("A withdrawal is attempted") {
            THEN("An exception is thrown for insufficient funds") {
                REQUIRE_THROWS_AS(
                    account.withdraw(50),
                    InsufficientFundsException
                );
            }
        }
    }
}

The CMake configuration for BDD tests is identical — catch_discover_tests() handles them automatically. The generated CTest names include the full scenario path (e.g., unit.Bank account deposits and withdrawals/A deposit of 100 is made/The balance is 100).

Matchers and Generators

Catch2 v3 provides powerful matchers for expressive assertions and generators for property-based testing. Both features require additional headers:

// test_matchers.cpp — Matchers and Generators
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <catch2/matchers/catch_matchers_vector.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/generators/catch_generators_range.hpp>
#include <string>
#include <vector>

using namespace Catch::Matchers;

TEST_CASE("String matchers", "[matchers]") {
    std::string greeting = "Hello, World!";
    REQUIRE_THAT(greeting, StartsWith("Hello"));
    REQUIRE_THAT(greeting, EndsWith("World!"));
    REQUIRE_THAT(greeting, ContainsSubstring("lo, Wo"));
}

TEST_CASE("Floating point matchers", "[matchers]") {
    double result = 1.0 / 3.0;
    REQUIRE_THAT(result, WithinAbs(0.333, 0.001));
    REQUIRE_THAT(result, WithinRel(0.333333, 0.0001));
}

TEST_CASE("Vector matchers", "[matchers]") {
    std::vector<int> v = {1, 2, 3, 4, 5};
    REQUIRE_THAT(v, VectorContains(3));
    REQUIRE_THAT(v, SizeIs(5));
}

TEST_CASE("Generators for property testing", "[generators]") {
    auto value = GENERATE(range(1, 100));

    CAPTURE(value);
    REQUIRE(value * value >= 0);    // Always true
    REQUIRE(value + 1 > value);      // No overflow in range
}

TEST_CASE("Table-driven tests with generators", "[generators]") {
    auto [input, expected] = GENERATE(table<int, int>({
        {1, 1},
        {2, 4},
        {3, 9},
        {4, 16},
        {10, 100}
    }));

    REQUIRE(input * input == expected);
}

Benchmarking Support

Catch2 v3 includes a built-in micro-benchmarking framework that integrates with the same test executable. Enable it with the CATCH_CONFIG_ENABLE_BENCHMARKING compile definition or include the appropriate header:

# tests/CMakeLists.txt — Benchmarks with Catch2
add_executable(benchmarks
    bench_sort.cpp
    bench_search.cpp
)
target_link_libraries(benchmarks PRIVATE
    Catch2::Catch2WithMain
    mylib
)

# Register benchmarks separately from unit tests
catch_discover_tests(benchmarks
    TEST_PREFIX "bench."
    PROPERTIES
        LABELS "benchmark"
        TIMEOUT 300
)
// bench_sort.cpp — Catch2 benchmarking
#include <catch2/catch_test_macros.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>
#include <algorithm>
#include <numeric>
#include <random>
#include <vector>

TEST_CASE("Sorting algorithm benchmarks", "[benchmark]") {
    std::mt19937 rng(42);

    BENCHMARK("std::sort 1000 elements") {
        std::vector<int> data(1000);
        std::iota(data.begin(), data.end(), 0);
        std::shuffle(data.begin(), data.end(), rng);
        std::sort(data.begin(), data.end());
        return data;
    };

    BENCHMARK("std::stable_sort 1000 elements") {
        std::vector<int> data(1000);
        std::iota(data.begin(), data.end(), 0);
        std::shuffle(data.begin(), data.end(), rng);
        std::stable_sort(data.begin(), data.end());
        return data;
    };

    BENCHMARK_ADVANCED("Custom sort with setup")(Catch::Benchmark::Chronometer meter) {
        std::vector<int> data(10000);
        std::iota(data.begin(), data.end(), 0);
        std::shuffle(data.begin(), data.end(), rng);

        meter.measure([&data] {
            auto copy = data;
            std::sort(copy.begin(), copy.end());
            return copy;
        });
    };
}
Tip: Separate benchmark tests from unit tests using different executables and CTest labels. This lets CI run fast unit tests on every commit while benchmarks run on a schedule or release branches only.

Single-Header Fallback

For projects that cannot use FetchContent (air-gapped environments, strict vendoring policies), Catch2 v2's single-header approach still works. However, for v3, you must vendor the entire source tree:

# Option A: Vendored Catch2 v3 source tree
# Place Catch2 source under third_party/Catch2/
add_subdirectory(third_party/Catch2)

add_executable(tests test_main.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)

list(APPEND CMAKE_MODULE_PATH third_party/Catch2/extras)
include(Catch)
catch_discover_tests(tests)
# Option B: Catch2 v2 single-header (legacy)
# Place catch.hpp under third_party/catch2/
add_library(catch2_main STATIC third_party/catch2/catch_main.cpp)
target_include_directories(catch2_main PUBLIC third_party/catch2)

add_executable(tests test_suite.cpp)
target_link_libraries(tests PRIVATE catch2_main mylib)

# Manual test registration (no catch_discover_tests in v2 single-header)
add_test(NAME unit_tests COMMAND tests)
// third_party/catch2/catch_main.cpp (v2 single-header)
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
Pitfall: The single-header Catch2 v2 approach does not support catch_discover_tests(). You'll need to register tests manually with add_test() or parse output yourself. Strongly prefer v3 with FetchContent for new projects.