Skip to main content
v2026.11,986 entries · CC-BY 4.0

Catch2: C++ Testing for Research Software

Catch2 provides C++ unit testing for research software. Configure CMake FetchContent, run floating-point assertions, and discover test suites with CTest.

Written and maintained by CASRAI Editorial Board

Published

On this page

Catch2 (C++ Automated Test Cases in Headers) is a modern, expressive unit testing, Test-Driven Development (TDD), and Behavior-Driven Development (BDD) framework for C++14, C++17, and C++20. Designed to minimize boilerplate, Catch2 decomposes standard C++ expressions directly inside test assertions, eliminating the need for specialized comparison macros. For scientific computing teams and research software engineers (RSEs), Catch2 provides seamless build-system integration via CMake, native floating-point tolerance matchers for numerical simulations, and automated test discovery in continuous integration pipelines.

What it is used for in research

In computational research, software correctness directly underpins scientific validity. Whether developing climate models, molecular dynamics engines, astrophysics simulations, or bioinformatics pipelines, unhandled numerical divergence or regressions compromise published findings. Catch2 serves as a core quality assurance foundation across computational research workflows:

  • Scientific Simulation Verification: Research software frequently solves differential equations, linear algebra systems, and Monte Carlo stochastic algorithms. Catch2 provides dedicated floating-point matchers (such as relative and absolute margin comparisons) that allow researchers to verify numerical convergence without falling prey to IEEE 754 precision representation errors. This supports robust numerical accuracy before submitting manuscripts or code to the Journal of Open Source Software (JOSS).
  • Regression Testing in Long-Lived Codebases: Academic software often evolves across successive cohorts of doctoral researchers and postdocs. Catch2 enables continuous integration (CI) test suites that automatically flag unintended side effects when optimizing performance-critical algorithms or refactoring legacy C++ routines, complementing formal verification techniques.
  • Modular Test Isolation via Hierarchical Sections: Scientific experiments often require initializing complex test fixtures, such as grid meshes or particle coordinate tables. Catch2 test cases support local nested SECTION blocks where each section executes independently from the top of the enclosing test case, eliminating the brittle setup and teardown classes mandated by older testing frameworks.
  • Peer Review and Reproducibility Audits: Modern scientific publishing guidelines increasingly mandate adherence to the FAIR4RS principles for research software. Distributing readable, automated unit tests alongside published repositories enables reviewers to verify software correctness and guarantees computational reproducibility across different operating systems and compiler toolchains.
  • Integration with Research Software Review: Catch2 generates standardized machine-readable test outputs (such as JUnit XML and SonarQube reports) that streamline automated pull-request validation during code review for research software projects.

Catch2 is distributed under the Boost Software License 1.0, an OSI-approved permissive license that facilitates adoption in both open-source and proprietary academic research partnerships, fully compliant with modern open-source software development practices.

Install or access

Starting with version 3 (Catch2 v3), the framework transitioned from a monolithic single-header file to a statically compiled library with modular headers. This transition substantially accelerates incremental build times in multi-file scientific projects. Catch2 can be incorporated into research projects through several official mechanisms.

For reproducible computational pipelines, using CMake’s built-in FetchContent module is the standard approach. It ensures all collaborators and CI build nodes download the exact specified release without requiring manual system package installations. Add the following block to your CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(ResearchSimulation CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)
FetchContent_Declare(
    Catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG        v3.5.2
)
FetchContent_MakeAvailable(Catch2)

Method 2: Package Managers (vcpkg and Conan)

For scientific projects managing complex multi-library dependency trees (such as Boost, Eigen, and HDF5), Catch2 is officially packaged in C++ dependency managers:

  • vcpkg: Run vcpkg install catch2, and link find_package(Catch2 3 REQUIRED) in your build configuration.
  • Conan: Add catch2/3.5.2 to your conanfile.txt dependencies.

Method 3: Linux System Package Repositories

On Debian, Ubuntu, and Fedora HPC clusters or workstations, system-wide versions can be installed via standard package managers:

# Ubuntu / Debian
sudo apt update && sudo apt install catch2

# Fedora / Red Hat Enterprise Linux
sudo dnf install catch2-devel

Step-by-step task

This tutorial walks through implementing an automated unit testing suite for a numerical research calculation (a Lennard-Jones interatomic potential calculator) using Catch2 v3, CMake, and CTest.

Step 1: Structure the Project Directory

Organize your repository to separate core numerical algorithms from test suites:

simulation_project/
├── CMakeLists.txt
├── include/
│   └── potential.hpp
├── src/
│   └── potential.cpp
└── tests/
    ├── CMakeLists.txt
    └── test_potential.cpp

Step 2: Define the Scientific Function

In include/potential.hpp, define a simple function to compute the Lennard-Jones 12-6 potential: V(r) = 4ε × [(σ/r)¹² − (σ/r)⁶].

#pragma once
#include <cmath>
#include <stdexcept>

inline double lennard_jones_potential(double r, double epsilon, double sigma) {
    if (r <= 0.0) {
        throw std::invalid_argument("Interatomic distance r must be positive.");
    }
    double term = sigma / r;
    double term6 = std::pow(term, 6);
    double term12 = term6 * term6;
    return 4.0 * epsilon * (term12 - term6);
}

Step 3: Author Catch2 Unit Tests with Floating-Point Matchers

Create tests/test_potential.cpp. In Catch2 v3, core assertions are imported via <catch2/catch_test_macros.hpp>, and floating-point comparisons require <catch2/matchers/catch_matchers_floating_point.hpp>:

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "potential.hpp"

TEST_CASE("Lennard-Jones potential evaluations", "[physics][numerical]") {
    const double epsilon = 1.0;
    const double sigma = 1.0;

    SECTION("Potential crosses zero at r = sigma") {
        double v = lennard_jones_potential(1.0, epsilon, sigma);
        REQUIRE_THAT(v, Catch::Matchers::WithinAbs(0.0, 1e-12));
    }

    SECTION("Potential reaches minimum at r = 2^(1/6) * sigma") {
        double r_min = std::pow(2.0, 1.0 / 6.0) * sigma;
        double v_min = lennard_jones_potential(r_min, epsilon, sigma);
        // Minimum depth is exactly -epsilon
        REQUIRE_THAT(v_min, Catch::Matchers::WithinRel(-1.0, 1e-7));
    }

    SECTION("Potential approaches repulsive core at short distances") {
        double v_short = lennard_jones_potential(0.8, epsilon, sigma);
        CHECK(v_short > 0.0);
    }

    SECTION("Zero or negative distance throws invalid_argument") {
        REQUIRE_THROWS_AS(lennard_jones_potential(0.0, epsilon, sigma), std::invalid_argument);
        REQUIRE_THROWS_AS(lennard_jones_potential(-1.5, epsilon, sigma), std::invalid_argument);
    }
}

Step 4: Configure CMake and CTest Registration

In tests/CMakeLists.txt, link against Catch2::Catch2WithMain to automatically provide a standard test runner entry point without writing a custom main() function. Then call catch_discover_tests for automated CTest registration:

add_executable(run_unit_tests test_potential.cpp)

target_include_directories(run_unit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)

# Catch2::Catch2WithMain provides the entry point main()
target_link_libraries(run_unit_tests PRIVATE Catch2::Catch2WithMain)

include(CTest)
include(Catch)
catch_discover_tests(run_unit_tests)

Step 5: Compile and Execute the Test Suite

Build the test binary using CMake from your terminal:

cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel

Execute the compiled test binary directly, or run the suite via CTest:

# Direct execution with verbose success output
./build/tests/run_unit_tests -s

# Automated CTest execution
ctest --test-dir build --output-on-failure

Output and how to read it

Catch2 provides readable terminal formatting designed to pinpoint failure locations and inspect exact values evaluated during runtime.

Standard Console Output for Passing Tests

When running with the -s (or --success) flag, Catch2 lists all passing assertions followed by a run summary:

===============================================================================
All tests passed (5 assertions in 1 test case)

Diagnosing Assertion Failures

When an assertion fails, Catch2 decomposes the expression, displaying the exact file, line number, captured expression, and expanded runtime values:

-------------------------------------------------------------------------------
Lennard-Jones potential evaluations
  Potential reaches minimum at r = 2^(1/6) * sigma
-------------------------------------------------------------------------------
tests/test_potential.cpp:18
...............................................................................

tests/test_potential.cpp:21: FAILED:
  REQUIRE_THAT( v_min, Catch::Matchers::WithinRel(-1.0, 1e-7) )
with expansion:
  -0.9875412 is within 0.00001% of -1.0

===============================================================================
test cases: 1 | 1 failed
assertions: 3 | 2 passed | 1 failed

In this failure report, Catch2 clarifies that the failure occurred inside the nested SECTION("Potential reaches minimum...") at line 21, showing that v_min evaluated to -0.9875412 rather than the expected -1.0 within the specified relative margin.

Generating Machine-Readable CI Output

For automated research software pipelines, Catch2 can format test results using the -r (or --reporter) flag:

# Output JUnit XML report for CI/CD dashboards
./build/tests/run_unit_tests -r junit -o junit-report.xml

# Output SonarQube format for static analysis platforms
./build/tests/run_unit_tests -r sonarqube -o sonarqube-report.xml

Troubleshooting

Undefined Reference to main

Symptom: Linker error: undefined reference to 'main' or unresolved external symbol _main.
Root Cause: Linking against Catch2::Catch2 instead of Catch2::Catch2WithMain, or omitting #define CATCH_CONFIG_MAIN in legacy v2 setups.
Resolution: In your CMakeLists.txt, ensure the test executable links against Catch2::Catch2WithMain. Alternatively, if writing a custom runner, include <catch2/catch_session.hpp> and provide an explicit int main(int argc, char* argv[]) entry point.

Slow Incremental Compilation Times

Symptom: Recompiling a single test file causes long rebuild times across the entire test suite.
Root Cause: Including <catch2/catch_all.hpp> in multiple test translation units, forcing the compiler to parse all matchers, generators, and reporters repeatedly.
Resolution: Replace <catch2/catch_all.hpp> with modular header inclusions: use <catch2/catch_test_macros.hpp> for standard tests and only include specific headers (such as <catch2/matchers/catch_matchers_floating_point.hpp>) in the specific test files that require them.

Floating-Point Equality Failures

Symptom: REQUIRE(computed == expected) fails intermittently across different CPU architectures, compiler optimization levels (-O2 vs -O3), or math libraries.
Root Cause: Exact binary equality comparisons fail due to rounding variances and fused multiply-add (FMA) instructions in floating-point hardware.
Resolution: Never use == on floating-point variables. Use Catch2 matchers: REQUIRE_THAT(val, Catch::Matchers::WithinRel(target, 1e-6)) for relative tolerances or Catch::Matchers::WithinAbs(target, 1e-9) for values near zero.

CTest Discovers Zero Tests

Symptom: ctest reports No tests were found!!! despite test binaries building successfully.
Root Cause: catch_discover_tests() runs the test executable during CMake configuration or post-build to list test cases via the binary’s --list-test-names-only flag; if cross-compiling or dynamic shared libraries are not on the system library path (LD_LIBRARY_PATH), discovery fails silently.
Resolution: Verify the test executable runs directly from the terminal. If dynamic dependencies are missing, set set(CTEST_ENVIRONMENT "LD_LIBRARY_PATH=...") or verify shared library paths in CMake.

Free alternatives

Several free and open-source testing frameworks are available for C++ research software. The ideal choice depends on compilation speed constraints, dependency footprint, and test architecture requirements.

Framework License Architecture Primary Research Strengths Trade-Offs & Computational Overhead
Catch2 Boost Software License 1.0 (Free, permissive) Compiled library (v3) with modular headers Natural C++ expression decomposition, nested section fixture isolation, native floating-point matchers, CMake test discovery Moderate compilation time in very large test suites compared to minimal single-header libraries
Google Test (GTest) BSD 3-Clause (Free, open-source) Compiled library with separate GMock mocking engine Industry standard in massive enterprise codebases, extensive death-test and parameterization support Requires distinct assertion macros (EXPECT_EQ, ASSERT_NEAR), verbose class-based fixtures, heavier CMake setup
Doctest MIT License (Free, permissive) Single-header or modular lightweight framework Ultra-fast compile times, lowest runtime overhead, syntax modeled directly after Catch2 Smaller ecosystem of specialized matchers, fewer built-in reporters, smaller community maintenance base
Boost.Test Boost Software License 1.0 (Free, permissive) Part of the Boost C++ Libraries (header-only or compiled) Extensive integration for codebases already relying heavily on Boost libraries, rich execution monitors Complex macro system, steep learning curve, significant header inclusion overhead if Boost is not already required

For scientific research teams, Catch2 offers an optimal balance between expressive assertion syntax and rigorous test isolation. Teams prioritizing compilation speed in resource-constrained environments may evaluate Doctest, while projects interfacing with legacy industrial enterprise stacks typically maintain Google Test.

FAQ

What is the difference between REQUIRE and CHECK in Catch2?

Both macros evaluate standard C++ boolean expressions. However, REQUIRE halts the current test case immediately upon failure, preventing subsequent assertions or dangerous dereferences of uninitialized memory. In contrast, CHECK logs the failure details and continues executing remaining assertions within the test case, allowing researchers to observe multiple failure conditions across a single run.

How do Catch2 sections differ from traditional fixture classes?

In traditional testing frameworks like Google Test, test fixtures require defining a C++ class that inherits from ::testing::Test and overriding SetUp() and TearDown() virtual methods. Catch2 replaces this with hierarchical SECTION blocks within a single TEST_CASE. When a test case runs, Catch2 executes it from the top once for each leaf section, ensuring that local variables initialized before a section are re-initialized cleanly for every branch.

Can Catch2 execute tests matching specific tags?

Yes. Catch2 supports tagging test cases using bracketed strings in the second argument of TEST_CASE("name", "[tag1][tag2]"). From the command line, users can filter execution by passing tags directly: ./run_unit_tests [physics] runs all tests tagged with [physics], while ./run_unit_tests ~[numerical] excludes tests tagged with [numerical].

How can benchmark tests be executed in Catch2?

Catch2 provides a built-in micro-benchmarking engine via <catch2/benchmark/catch_benchmark.hpp>. By wrapping computational code in a BENCHMARK("label") { return algorithm(); }; block inside a test case, Catch2 automatically samples execution runs, measures system jitter, and outputs mean execution time and standard deviation, aiding computational complexity analysis in algorithmic research.

Sources

Follow CASRAI

Research-administration guidance, standards updates and independent tool reviews.

Ask CASRAI · free to try

Ask about Catch2: C++ Testing for Research Software

Ask your first 2 questions free below. Subscribers get 150 a day for $29 a month.

An AI assistant specialized in research administration. It cites the sources behind every answer, labels web answers and says when it can't answer.

Answers draw on CASRAI's guides and dictionary plus the federal and funder documents we index: Federal Register, Grants.gov, Regulations.gov and UKRI.

Works on this site and inside Claude, Cursor and the AI tools you already use.

Everything CASRAI publishes — this page, the dictionary, the guides and the news — stays free to read, with no account and no card.

Referenced across the research world

University of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logoUniversity of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logo
  • University of Cambridge logo
  • Columbia University logo
  • Crossref logo
  • University of Edinburgh logo
  • Harvard University logo
  • University of Oxford logo
  • Princeton University logo
  • Stanford School of Medicine logo
  • University College London logo
  • ORCID logo

View CASRAI adoption →