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.
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)
${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;
});
};
}
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"
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.