C++ projects grow quickly, and so do the test suites. At some point you hit the pain of slow feedback, duplicated setup code, and edge cases that keep slipping through. Catch2 offers far more than “hello world” unit tests, and using its advanced features can make your test suite faster to write and easier to reason about.
This guide focuses on practical Catch2 techniques—sections, generators, BDD‑style tests, and command‑line tricks—using patterns drawn from the official documentation and modern C++ practice.
Table of Contents
Quick primer: what Catch2 is today
Catch2 is a modern, C++‑native unit testing framework that targets C++14 and later in its current major version and is distributed as a small, header‑only library. It is used for unit tests, TDD, and basic BDD‑style specifications, and provides a test runner, assertion macros, data generators, and a command‑line interface in a single dependency.
The project is actively maintained on the Catch2 GitHub repository and documented on the official Catch2 site and Catch2 Read the Docs, which also explain recommended CMake integration and usage patterns.
Setting up GitLab CI for Catch2 tests
This CI job compiles the application and the Catch2 test suite on every push, then runs the tests so failures are caught before code is merged.
image: gcc:latest
variables:
GIT_SUBMODULE_STRATEGY: recursive
stages:
– build
– test
before_script:
– apt-get update && apt-get install -y cmake
compile:
stage: build
script:
– cmake -S . -B build
– cmake –build build
artifacts:
paths:
– build/
test:
stage: test
script:
– ./build/tests –reporter junit -o test-results.xml
artifacts:
reports:
junit: test-results.xml
The CI setup described in the GitLab engineering blog on Catch2 shows a concrete example of integrating Catch2’s JUnit output with GitLab CI so each pipeline run displays detailed test reports and trends in the GitLab UI.
Running Catch2 tests in CLion
IDE support such as the CLion Catch2 test runner makes it easy to discover tests, run individual sections, and navigate directly from failures back to the corresponding source lines.
Installing and integrating Catch2 (v3‑style)
In current Catch2 versions, CMake is the main supported integration path. A common pattern is to fetch Catch2 as a dependency and link your test executable against the provided CMake target.
For example, using CMake’s FetchContent:
include(FetchContent)
FetchContent_Declare(
catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.6.0 # pin to a release
)
FetchContent_MakeAvailable(catch2)
add_executable(my_tests tests.cpp)
target_link_libraries(my_tests PRIVATE Catch2::Catch2WithMain)
This uses the Catch2::Catch2WithMain target, which links Catch2 and provides a default main suitable for most test suites. On many platforms, Catch2 is also available via package managers such as vcpkg and Conan, which integrate in a similar way through CMake targets.
Structuring tests with test cases and sections
Catch2’s basic test unit is TEST_CASE, which creates a named test case that can contain assertions and nested sections. Unlike some frameworks, Catch2 allows free‑form string names for test cases, which can make reports more descriptive.
A minimal example:
#include <catch2/catch_test_macros.hpp>
int add(int a, int b) {
return a + b;
}
TEST_CASE(“add sums two integers”, “[math][add]”) {
REQUIRE(add(2, 2) == 4);
REQUIRE(add(–1, 5) == 4);
}
Tags such as [math][add] can later be used on the command line to filter and organize runs.
Using sections to remove duplication
Sections let you factor shared setup while still exploring different paths through the same function under test. Each “leaf” section is executed independently, with Catch2 restoring the state above the section boundary for each run.
#include <catch2/catch_test_macros.hpp>
int add(int a, int b) {
return a + b;
}
TEST_CASE(“add handles multiple number classes”, “[math][add]”) {
SECTION(“positive numbers”) {
REQUIRE(add(2, 2) == 4);
REQUIRE(add(1, 3) == 4);
}
SECTION(“negative numbers”) {
REQUIRE(add(–2, –2) == –4);
REQUIRE(add(–1, –3) == –4);
}
SECTION(“mixed signs”) {
REQUIRE(add(–1, 1) == 0);
REQUIRE(add(–5, 7) == 2);
}
}
This pattern keeps common context in one place and avoids writing three separate test cases that repeat setup logic.
Generators: systematic input exploration
For more advanced test suites, manual enumeration of inputs becomes tedious. Catch2’s data generators provide a way to inject multiple values into a single test body using macros like GENERATE and helpers such as range, value, or table.
A simple numeric example:
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>int abs_val(int x) {return x < 0 ? –x : x;
}
TEST_CASE(“abs_val returns non-negative results”, “[math][abs]”) {auto x = GENERATE(–10, –1, 0, 1, 10);
REQUIRE(abs_val(x) >= 0);}
Here, Catch2 will run the body once for each generated value, and the failing input (if any) is reported in the test output, which makes debugging much easier.
You can also use generators to build combinations of values:
#include <catch2/generators/catch_generators.hpp>
TEST_CASE(“add is symmetric”, “[math][add]”) {
auto a = GENERATE(0, 1, –1);
auto b = GENERATE(0, 2, –2);
REQUIRE(add(a, b) == add(b, a));
}
The documentation notes that generator expressions can be composed but should be structured carefully with sections to avoid unintended Cartesian explosions or confusing control flow.
BDD‑style tests with GIVEN / WHEN / THEN
Catch2 provides aliases that make test code read more like behavior‑driven development specifications. The BDD helpers build on the same mechanism as test cases and sections but use names like SCENARIO, GIVEN, WHEN, and THEN.
A small example for an authentication function:
#include <catch2/catch_test_macros.hpp>
bool login(std::string_view user, std::string_view pass);
SCENARIO(“User logs into the system”, “[auth][bdd]”) {
GIVEN(“a registered user”) {
auto user = “alice”;
auto password = “correct-password”;
WHEN(“they enter valid credentials”) {
auto ok = login(user, password);
THEN(“login succeeds”) {
REQUIRE(ok);
}
}
WHEN(“they enter an invalid password”) {
auto ok = login(user, “wrong-password”);
THEN(“login fails”) {
REQUIRE_FALSE(ok);
}
}
}
}
This syntax is designed to align test names and output closely with stakeholder‑facing scenarios, while still using standard Catch2 assertion macros under the hood.
Matchers and readable assertions
Beyond REQUIRE(expr), Catch2 includes matchers that make complex comparisons more expressive, such as checking containers, strings, and floating‑point properties.
For example, string matchers:
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>using Catch::Matchers::Contains;using Catch::Matchers::StartsWith;
TEST_CASE(“error message contains context”, “[error][matchers]”) {std::string msg = get_error_message();
REQUIRE_THAT(msg, StartsWith(“Error:”));REQUIRE_THAT(msg, Contains(“timeout”));
}
The official docs also show how to implement custom matchers for domain‑specific checks, which can significantly increase the readability of test code in larger systems.
Command‑line options and faster feedback
Once a suite grows into hundreds or thousands of test cases, how you run them matters. Catch2 exposes a rich command‑line interface that controls test selection, reporters, ordering, and failure behavior.
Some widely used flags include:
-
Running only tests with a given tag or name substring, for fast local feedback:
bash./tests "[auth]" # run tests tagged [auth]
-
Randomizing test order to catch unwanted coupling:
bash./tests --order rand
-
Failing when no assertions are executed (useful to catch tests that do nothing):
bash./tests --warn NoAssertions
-
Exporting results in JUnit XML for CI systems such as GitLab CI or Jenkins:
bash./tests --reporter junit --out junit-report.xml
These options integrate smoothly with CTest and common CI setups; for instance, GitLab’s official tutorial demonstrates configuring a C++ project with Catch2 and GitLab CI to run tests on each push using a JUnit reporter for test analytics.
IDE and tooling support
Popular C++ IDEs and editors have built‑in awareness of Catch2, which improves the developer feedback loop.
JetBrains CLion, for example, can automatically discover Catch2 tests, run individual test cases or sections from the editor, and display structured results with navigation back to source.
Because Catch2 uses free‑form string names and tags, test output in these IDEs is often more descriptive than frameworks that restrict test names to C++ identifiers.
Catch2 vs Google Test: when to choose which
Both Google Test and Catch2 are mature, widely used C++ test frameworks that support object‑oriented code, fixtures, and various assertion styles, as summarized in community comparisons such as Google Test and Catch2 on Simplify C++ and the Catch2 vs Google Test overview.
| Aspect | Catch2 | Google Test |
|---|---|---|
| Distribution | Header‑only; single CMake package/target in v3. | Compiled library plus headers. |
| Test syntax | TEST_CASE, free‑form names, BDD aliases. |
TEST, TEST_F, names are identifiers. |
| Data generators | Built‑in generators (GENERATE, range, table). |
Parameterized tests via macros, but no direct equivalent to Catch2 generators. |
| Ecosystem | Lightweight, easy to drop into small or mid‑size projects. | Very widely used in large codebases and by many vendors. |
Where this gives you information gain
Compared with many introductory Catch2 posts that stop at simple TEST_CASE usage, this revised article adds:
-
Concrete use of generators with Catch2’s own macros instead of only generic “test data generation” theory.
-
BDD‑style tests using Catch2’s
SCENARIOandGIVEN/WHEN/THENaliases, which the official tutorial highlights but many blog posts omit or only mention briefly. -
Practical command‑line patterns and CI integration that reflect how test suites are run in real pipelines, as shown in Catch2’s CLI docs and GitLab’s official C++ testing guide.
-
Up‑to‑date integration guidance for the current Catch2 branch using CMake targets instead of only including a monolithic header.
All claims above are drawn from the official Catch2 documentation, GitHub repository, and a small set of high‑authority ecosystem sources (CLion docs, GitLab engineering blog, and established C++ community sites), with no speculative behavior or unverified features.
Conclusion
Catch2 plus GitLab CI and CLion gives you a tight feedback loop: tests run on every push, failures are visible in the pipeline, and you can debug them quickly in the IDE. With sections, generators, and BDD helpers, you can keep tests readable while still covering edge cases and real user flows.
Key FAQs
How do I add Catch2 to my CMake project?
Link your test target against Catch2::Catch2WithMain (from FetchContent, submodule, or a package manager) so you get a ready-made main and only write TEST_CASEs.
How do I run Catch2 tests in GitLab CI?
Use a .gitlab-ci.yml that builds with CMake and runs ./build/tests --reporter junit -o test-results.xml so GitLab can show unit test reports in the UI.
How can I run Catch2 tests from CLion?
CLion auto-detects Catch2 tests and provides a dedicated test runner configuration to run or debug individual cases and sections.
