1. What is Catch2
Catch2 is a header-friendly C++ testing framework that gives you:
- A test runner that discovers and executes tests automatically
- Expressive assertions via
REQUIRE/CHECKwith automatic expression decomposition — no need to write a message, Catch2 shows you what both sides evaluated to - Sections for sharing setup across related tests without a separate fixture class
- BDD-style
SCENARIO/GIVEN/WHEN/THENblocks as a first-class alternative - Parameterised and typed tests via
TEST_CASE_METHOD,TEMPLATE_TEST_CASE, and generators - Built-in benchmarking with
BENCHMARK
The key philosophical difference from GoogleTest is expression decomposition: when you write REQUIRE(a == b), the framework captures both a and b separately and prints both in the failure message. You never need to choose between EXPECT_EQ, EXPECT_LT, EXPECT_STREQ — one macro handles all of them.
**v2 vs v3:** Catch2 v3 (the current major version) is a proper multi-file library, not a single-header include. It requires CMake 3.16+ and a C++14 compiler at minimum, though C++17 unlocks generators and several quality-of-life features. All examples in this guide target **v3**.
2. Install & Build Setup (CMake)
Recommended: FetchContent
No system install required. CMake downloads and builds Catch2 as part of your project.
cmake_minimum_required(VERSION 3.20)
project(MyProjectTests CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.3
)
FetchContent_MakeAvailable(Catch2)
enable_testing()
add_library(my_lib
src/math.cpp
include/math.hpp
)
target_include_directories(my_lib PUBLIC include)
add_executable(my_tests
tests/test_math.cpp
)
target_link_libraries(my_tests PRIVATE
my_lib
Catch2::Catch2WithMain # provides main() for you
)
include(CTest)
include(Catch)
catch_discover_tests(my_tests) # registers each TEST_CASE with ctest
Build and run:
cmake -S . -B build
cmake --build build -j
ctest --test-dir build --output-on-failure
**`Catch2::Catch2WithMain` vs `Catch2::Catch2`:** Link `Catch2WithMain` to get a pre-built `main()` that handles argument parsing, reporter selection, and test discovery. Use `Catch2::Catch2` only if you need a custom `main()` — for example, to initialise a global resource before any tests run.
Alternative: system package
# Ubuntu / Debian
sudo apt install catch2
# macOS
brew install catch2
Then in CMakeLists.txt replace the FetchContent block with:
find_package(Catch2 3 REQUIRED)
3. Your First Test
Let's use the same math example as the GTest guide so you can compare the two side by side.
include/math.hpp
#pragma once
int add(int a, int b);
int divide(int a, int b); // throws on b == 0
src/math.cpp
#include "math.hpp"
#include <stdexcept>
int add(int a, int b) { return a + b; }
int divide(int a, int b) {
if (b == 0) throw std::invalid_argument("division by zero");
return a / b;
}
tests/test_math.cpp
#include <catch2/catch_all.hpp>
#include "math.hpp"
TEST_CASE("add works", "[math]") {
REQUIRE(add(2, 3) == 5);
REQUIRE(add(-2, 2) == 0);
}
TEST_CASE("divide works", "[math]") {
REQUIRE(divide(10, 2) == 5);
}
TEST_CASE("divide by zero throws", "[math][exceptions]") {
REQUIRE_THROWS_AS(divide(10, 0), std::invalid_argument);
}
Key idea: `REQUIRE` vs `CHECK`
These are Catch2's equivalents of ASSERT_ and EXPECT_ in GoogleTest:
| Macro | On failure | GTest equivalent |
|---|---|---|
REQUIRE(expr) | Stops the test immediately | ASSERT_*() |
CHECK(expr) | Records failure, test continues | EXPECT_*() |
REQUIRE_FALSE(expr) | Stops if expr is true | ASSERT_FALSE() |
CHECK_FALSE(expr) | Records if expr is true | EXPECT_FALSE() |
Use REQUIRE when continuing after a failure would cause a crash or produce misleading subsequent failures (dereferencing a pointer, accessing a nullopt, etc.). Use CHECK when you want to see all failures in one run.
Expression decomposition in action
When this fails:
REQUIRE(result.size() == 3);
Catch2 prints:
FAILED:
REQUIRE( result.size() == 3 )
with expansion:
5 == 3
No custom message needed. The framework decomposes both sides automatically.
Tags
The second argument to TEST_CASE is a space-separated list of tags in square brackets: "[math][fast]". Tags are used to filter which tests run.
| Tag | Meaning |
|---|---|
[math] | Custom category tag |
[!hide] or [.] | Exclude from default run |
[!mayfail] | Mark an expected failure |
[!throws] | Expect test body to throw |
4. Assertions Reference
Equality and comparisons
Catch2 uses a single REQUIRE / CHECK for all comparison operators:
REQUIRE(a == b);
REQUIRE(a != b);
REQUIRE(a < b);
REQUIRE(a <= b);
REQUIRE(a > b);
REQUIRE(a >= b);
Floating-point
Don't use REQUIRE(a == b) for floats:
// Classic: Approx with relative/absolute epsilon
REQUIRE(result == Catch::Approx(3.14).epsilon(1e-6));
REQUIRE(result == Catch::Approx(100.0).margin(0.5));
// Matcher style (v3 preferred):
REQUIRE_THAT(result, Catch::Matchers::WithinRel(3.14, 1e-6));
REQUIRE_THAT(result, Catch::Matchers::WithinAbs(100.0, 0.5));
String matchers
REQUIRE_THAT(s, Catch::Matchers::ContainsSubstring("hello"));
REQUIRE_THAT(s, Catch::Matchers::StartsWith("pre"));
REQUIRE_THAT(s, Catch::Matchers::EndsWith("fix"));
REQUIRE_THAT(s, Catch::Matchers::Matches("\\d{4}-\\d{2}-\\d{2}")); // regex
Range / collection matchers
#include <catch2/matchers/catch_matchers_range_equals.hpp>
std::vector<int> got = compute();
REQUIRE_THAT(got, Catch::Matchers::RangeEquals(std::vector<int>{ 1, 2, 3 }));
REQUIRE_THAT(got, Catch::Matchers::UnorderedRangeEquals(expected));
REQUIRE_THAT(got, Catch::Matchers::Contains(42));
REQUIRE_THAT(got, Catch::Matchers::IsEmpty());
REQUIRE_THAT(got, Catch::Matchers::SizeIs(3));
Composing matchers
// && and || work on matchers directly
REQUIRE_THAT(s,
Catch::Matchers::StartsWith("Error") &&
Catch::Matchers::ContainsSubstring("code 42"));
Adding context to failures
// INFO — only shown on failure
INFO("processing item index: " << i);
REQUIRE(items[i].valid());
// CAPTURE — shorthand for printing "variable = value"
CAPTURE(x, y, result);
REQUIRE(result == expected);
// prints: x = 7, y = 3, result = 22
**`CAPTURE` in loops** is Catch2's equivalent of GTest's `SCOPED_TRACE`. Drop it inside a loop and every failure message will include the loop variable automatically.
5. Sections & Fixtures
Sections — shared setup without a class
Catch2's biggest ergonomic win over GTest is SECTION. Every section within a TEST_CASE runs from the top — the code above the first SECTION acts as implicit per-section setup. No fixture class required.
#include <catch2/catch_all.hpp>
#include "bank.hpp"
TEST_CASE("BankAccount operations", "[bank]") {
BankAccount acc(100); // runs fresh before EACH section
SECTION("deposit increases balance") {
acc.deposit(50);
REQUIRE(acc.balance() == 150);
}
SECTION("withdraw decreases balance") {
acc.withdraw(40);
REQUIRE(acc.balance() == 60);
}
SECTION("overdraft throws") {
REQUIRE_THROWS_AS(acc.withdraw(200), std::runtime_error);
}
SECTION("nested: deposit then withdraw") {
acc.deposit(100);
SECTION("full withdrawal") {
acc.withdraw(200);
REQUIRE(acc.balance() == 0);
}
SECTION("partial withdrawal") {
acc.withdraw(50);
REQUIRE(acc.balance() == 150);
}
}
}
Catch2 runs the TEST_CASE body once per leaf section. The nesting creates a test tree — so acc is always constructed fresh at balance 100, even in nested cases.
TEST_CASE_METHOD — class-based fixtures
When you want a full OOP fixture (shared state that needs a constructor/destructor, or inheritance), use TEST_CASE_METHOD:
struct BankFixture {
BankAccount acc{ 100 };
void fund(int extra) { acc.deposit(extra); }
};
TEST_CASE_METHOD(BankFixture, "deposit via fixture", "[bank]") {
fund(50);
REQUIRE(acc.balance() == 150);
}
TEST_CASE_METHOD(BankFixture, "overdraft via fixture", "[bank]") {
REQUIRE_THROWS(acc.withdraw(200));
}
**Sections vs TEST_CASE_METHOD:** Prefer `SECTION` for most cases — it keeps the fixture and tests in one place. Use `TEST_CASE_METHOD` when the fixture logic is complex enough to warrant its own class, or when you need to share it across multiple test files.
6. BDD Style
Catch2 ships BDD-style macros as first-class syntax. They are syntactic sugar over TEST_CASE and SECTION, so everything you know about sections applies.
#include <catch2/catch_all.hpp>
#include "bank.hpp"
SCENARIO("a customer makes deposits and withdrawals", "[bank][bdd]") {
GIVEN("an account with 100 balance") {
BankAccount acc(100);
WHEN("the customer deposits 50") {
acc.deposit(50);
THEN("the balance is 150") {
REQUIRE(acc.balance() == 150);
}
}
WHEN("the customer withdraws more than the balance") {
THEN("an exception is thrown") {
REQUIRE_THROWS_AS(acc.withdraw(500), std::runtime_error);
}
}
}
}
When this runs, Catch2 produces:
Scenario: a customer makes deposits and withdrawals
Given: an account with 100 balance
When: the customer deposits 50
Then: the balance is 150
Scenario: a customer makes deposits and withdrawals
Given: an account with 100 balance
When: the customer withdraws more than the balance
Then: an exception is thrown
BDD style is most useful when non-engineers read test output (QA, product managers) or when the test descriptions serve as living documentation for domain rules. For pure unit tests of internal functions, plain TEST_CASE / SECTION is usually cleaner.
7. Parameterised Tests
TEMPLATE_TEST_CASE — typed tests
Run the same test body for multiple types:
TEMPLATE_TEST_CASE("numeric add is commutative", "[math][template]",
int, long, float, double)
{
TestType a = static_cast<TestType>(3);
TestType b = static_cast<TestType>(7);
REQUIRE(add(a, b) == add(b, a));
}
Generators — data-driven tests
Generators are Catch2's answer to INSTANTIATE_TEST_SUITE_P:
#include <catch2/generators/catch_generators.hpp>
#include <catch2/generators/catch_generators_adapters.hpp>
TEST_CASE("add with generated values", "[math][gen]") {
auto [a, b, expected] = GENERATE(table<int, int, int>({
{ 1, 2, 3 },
{ -1, 1, 0 },
{ 10, 5, 15 },
}));
CAPTURE(a, b, expected);
REQUIRE(add(a, b) == expected);
}
Built-in generator helpers
// Integer range
auto x = GENERATE(range(1, 10)); // 1..9 (exclusive end)
// Explicit values
auto v = GENERATE(1, 2, 4, 8, 16);
// Random integers (seeded by --rng-seed)
auto r = GENERATE(take(5, random(0, 100)));
// String values
auto path = GENERATE("/api/v1", "/api/v2", "/health");
// Cartesian product of two generators
auto [i, j] = GENERATE(cartesian_product(
std::vector{ 1, 2, 3 },
std::vector{ 10, 20 }
));
**Generators and sections interact:** When you use both `GENERATE` and `SECTION` in the same test case, the body runs for every combination of generator value × section. This is powerful but can create a large number of runs. Prefer one or the other when the test tree would be hard to read.
8. Exception Testing
Core exception macros
| Macro | Passes when |
|---|---|
REQUIRE_THROWS(expr) | Any exception is thrown |
REQUIRE_THROWS_AS(expr, Type) | An exception of exactly Type (or subclass) is thrown |
REQUIRE_THROWS_WITH(expr, str) | Exception thrown and what() matches the string/matcher |
REQUIRE_THROWS_MATCHES(expr, Type, matcher) | Type matches AND the exception object satisfies a matcher |
REQUIRE_NOTHROW(expr) | No exception is thrown |
CHECK_THROWS_AS(...) | Same as above but test continues on failure |
Practical examples
TEST_CASE("exception testing", "[exceptions]") {
SECTION("any exception") {
REQUIRE_THROWS(divide(1, 0));
}
SECTION("specific type") {
REQUIRE_THROWS_AS(divide(1, 0), std::invalid_argument);
}
SECTION("type + message substring") {
REQUIRE_THROWS_WITH(
divide(1, 0),
Catch::Matchers::ContainsSubstring("zero")
);
}
SECTION("type + custom matcher on the exception object") {
REQUIRE_THROWS_MATCHES(
divide(1, 0),
std::invalid_argument,
Catch::Matchers::Message("division by zero")
);
}
SECTION("no exception on valid input") {
REQUIRE_NOTHROW(divide(10, 2));
}
}
Inspecting exception fields manually
When you need to inspect the exception object beyond its message:
TEST_CASE("inspect exception fields", "[exceptions]") {
try {
riskyOperation();
FAIL("expected an exception but none was thrown");
} catch (const MyDomainError& e) {
REQUIRE(e.code() == 503);
REQUIRE(e.retryable() == true);
}
}
9. Mocking with trompeloeil
Catch2 does not ship a mocking framework, but it integrates cleanly with trompeloeil — a header-only C++ mock library designed to work alongside it.
CMake setup
FetchContent_Declare(
trompeloeil
GIT_REPOSITORY https://github.com/rollbear/trompeloeil.git
GIT_TAG v47
)
FetchContent_MakeAvailable(trompeloeil)
target_link_libraries(my_tests PRIVATE
Catch2::Catch2WithMain
trompeloeil::trompeloeil
)
Activate the Catch2 integration by including the bridge header once in your test target:
#include <catch2/catch_all.hpp>
#include <catch2/trompeloeil.hpp>
Production code
// include/worker.hpp
#pragma once
#include <string>
struct ILogger {
virtual ~ILogger() = default;
virtual void info(const std::string& msg) = 0;
virtual void error(const std::string& msg) = 0;
};
class Worker {
public:
explicit Worker(ILogger& log) : log_(log) {}
bool run(int x) {
if (x < 0) { log_.error("x is negative"); return false; }
log_.info("running");
return true;
}
private:
ILogger& log_;
};
Tests with trompeloeil
#include <catch2/catch_all.hpp>
#include <catch2/trompeloeil.hpp>
#include "worker.hpp"
class MockLogger : public ILogger {
public:
MAKE_MOCK1(info, void(const std::string&), override);
MAKE_MOCK1(error, void(const std::string&), override);
};
TEST_CASE("Worker logs error on negative input", "[worker]") {
MockLogger log;
Worker w(log);
REQUIRE_CALL(log, error(trompeloeil::re("negative")))
.TIMES(1);
FORBID_CALL(log, info(trompeloeil::_));
REQUIRE(w.run(-1) == false);
}
TEST_CASE("Worker logs info on valid input", "[worker]") {
MockLogger log;
Worker w(log);
REQUIRE_CALL(log, info("running"))
.TIMES(1);
FORBID_CALL(log, error(trompeloeil::_));
REQUIRE(w.run(5) == true);
}
Key trompeloeil matchers
| Matcher | Meaning |
|---|---|
trompeloeil::_ | Match any argument (wildcard) |
trompeloeil::eq(v) | Equals v |
trompeloeil::ne(v) | Not equal to v |
trompeloeil::gt(v) | Greater than v |
trompeloeil::re("pattern") | String matches regex |
trompeloeil::any_of(a, b) | Equals any of the listed values |
**Prefer lightweight fakes over mocks.** For simple dependencies like loggers or counters, a hand-written fake struct (a plain class that records calls in a vector) is often more readable than a mock framework. Reach for trompeloeil when you need to verify call order, argument matchers, or return value sequences.
10. Running & Filtering Tests
Running the binary directly
# Run all tests
./build/my_tests
# Verbose: show test names as they run
./build/my_tests -v
# List all tests without running
./build/my_tests --list-tests
# List all tags
./build/my_tests --list-tags
Filtering by tag
# Only tests tagged [math]
./build/my_tests "[math]"
# Tests tagged [math] but NOT [slow]
./build/my_tests "[math][~slow]"
# Any test tagged [unit] OR [fast]
./build/my_tests "[unit],[fast]"
# All tests except those tagged [integration]
./build/my_tests "~[integration]"
Filtering by name
# Wildcard match on test case name
./build/my_tests "*divide*"
./build/my_tests "add*"
Useful flags
| Flag | Effect |
|---|---|
--abort | Stop after the first failure |
-x N | Stop after N failures |
--rng-seed 42 | Fix random seed for reproducible generator tests |
--rng-seed time | Use current time as seed (default) |
-r xml | Output JUnit XML (for CI integration) |
-r compact | Terse output format |
--durations yes | Print time taken per test |
--benchmark-no-analysis | Skip statistical analysis in benchmarks |
Running via ctest
# Run all (parallel)
ctest --test-dir build -j8 --output-on-failure
# Filter by label (ctest sees Catch2 tags as labels)
ctest --test-dir build -L math
# Rerun only failing tests
ctest --test-dir build --rerun-failed
Repeat until failure (flaky test hunting)
for i in $(seq 100); do
./build/my_tests --abort || break
done
11. A Real Example: Testing a Parser
The same parse_kv function from the GTest guide, tested with Catch2 idioms.
include/config.hpp
#pragma once
#include <optional>
#include <string>
#include <string_view>
struct Kv { std::string key; std::string value; };
// Parses "key=value" (no spaces). Returns nullopt if invalid.
std::optional<Kv> parse_kv(std::string_view line);
tests/test_config.cpp
#include <catch2/catch_all.hpp>
#include "config.hpp"
TEST_CASE("parse_kv — valid input", "[config]") {
auto kv = parse_kv("mode=fast");
REQUIRE(kv.has_value());
REQUIRE(kv->key == "mode");
REQUIRE(kv->value == "fast");
}
TEST_CASE("parse_kv — invalid inputs", "[config]") {
SECTION("missing equals sign") {
REQUIRE_FALSE(parse_kv("modefast").has_value());
}
SECTION("empty key") {
REQUIRE_FALSE(parse_kv("=fast").has_value());
}
SECTION("empty value") {
REQUIRE_FALSE(parse_kv("mode=").has_value());
}
SECTION("empty string") {
REQUIRE_FALSE(parse_kv("").has_value());
}
}
// Generator-based table test — equivalent to GTest's INSTANTIATE_TEST_SUITE_P
TEST_CASE("parse_kv — valid pairs round-trip", "[config][gen]") {
auto [input, exp_key, exp_val] = GENERATE(table<
std::string_view, std::string_view, std::string_view>({
{ "mode=fast", "mode", "fast" },
{ "timeout=30", "timeout", "30" },
{ "host=0.0.0.0", "host", "0.0.0.0" },
}));
CAPTURE(input);
auto kv = parse_kv(input);
REQUIRE(kv.has_value());
REQUIRE(kv->key == exp_key);
REQUIRE(kv->value == exp_val);
}
Designing for testability — inject dependencies
Never call global time, filesystem, or randomness functions directly inside your logic. Inject them.
using NowFn = std::function<std::chrono::system_clock::time_point()>;
class Token {
public:
explicit Token(NowFn now) : now_(std::move(now)) {}
bool expired(std::chrono::seconds ttl) const {
return (now_() - created_) > ttl;
}
private:
NowFn now_;
std::chrono::system_clock::time_point created_{ std::chrono::system_clock::now() };
};
// In the test:
TEST_CASE("Token expiry", "[token]") {
using namespace std::chrono_literals;
auto fixed = std::chrono::system_clock::now();
SECTION("not expired when fresh") {
Token t([&] { return fixed; });
REQUIRE_FALSE(t.expired(60s));
}
SECTION("expired after ttl passes") {
Token t([&] { return fixed + 120s; });
REQUIRE(t.expired(60s));
}
}
12. Best Practices
Naming conventions
Catch2 test names are free-form strings. Use a consistent pattern so names are scannable:
// subject — condition — expected outcome
TEST_CASE("parse_kv — empty key — returns nullopt", "[config]");
TEST_CASE("BankAccount — overdraft — throws runtime_error", "[bank]");
TEST_CASE("divide — by zero — throws invalid_argument", "[math]");
Arrange / Act / Assert
TEST_CASE("Encoder round-trips valid data", "[encoder]") {
// Arrange
Encoder enc;
std::string input = "hello world";
// Act
auto encoded = enc.encode(input);
auto decoded = enc.decode(encoded);
// Assert
REQUIRE(decoded == input);
}
One behaviour per test
Keep each TEST_CASE or SECTION focused on a single observable behaviour. If a test fails, you want the name alone to tell you what broke.
Things to avoid
| Anti-pattern | Why it hurts | Fix |
|---|---|---|
| Global / static mutable state in tests | Tests affect each other, order-dependent failures | Use SECTION with local variables, or fixture constructors |
Calling system_clock::now() in production code directly | Tests are timing-dependent and flaky | Inject a NowFn or clock abstraction |
Asserting on what() strings with == | Exception messages change; brittle tests | Use ContainsSubstring or REQUIRE_THROWS_AS for type only |
One enormous TEST_CASE with no sections | Hard to pinpoint failures | Split into sections or separate TEST_CASEs |
Leaving [.]-hidden tests indefinitely | Accumulates dead code nobody runs | Fix the test or delete it; treat [.] as a TODO with a deadline |
Using GENERATE(random(...)) without a fixed seed in CI | Non-reproducible failures | Pass --rng-seed 12345 in the CI pipeline |
Mini checklist: when your Catch2 suite is in good shape
You're in a great place when:
- Running the full suite takes under a minute
- A failing test name and Catch2's expansion output tell you exactly what went wrong without reading source
- Tests pass in any order — no shared mutable state, no filesystem residue
- Changing an implementation detail does not break tests unless observable behaviour changes
- Every test passes deterministically — flaky tests are treated as broken tests and fixed immediately
- Test names and section names read like a specification of the module's contract