How Catch2 Test Case Registration Works Internally: Static Initialization and Registry Architecture
Catch2 registers test cases at static initialization time through the TEST_CASE macro, which creates a static AutoReg object that constructs TestCaseInfo and ITestInvoker instances and registers them with the global TestRegistry before main() executes.
Catch2 (the modern C++ testing framework from catchorg/Catch2) eliminates manual test registration through an elegant static initialization mechanism. Understanding how Catch2 test case registration works internally reveals why you can simply write TEST_CASE macros without any explicit registration boilerplate. This article explores the macro expansion, registry architecture, and execution flow based on the latest source code in the devel branch.
The Entry Point: TEST_CASE Macro and AutoReg
The journey begins in src/catch2/catch_test_macros.hpp, where the TEST_CASE macro expands to INTERNAL_CATCH_TESTCASE. This internal macro generates a uniquely-named static object using INTERNAL_CATCH_UNIQUE_NAME and __COUNTER__ to ensure uniqueness across translation units.
The generated static object is of type Catch::detail::AutoReg. Because this object has static storage duration, its constructor runs during the static initialization phase—guaranteed to execute before main() begins. This is the mechanism that enables "zero-boilerplate" test discovery: you write the macro, and the compiler inserts the registration code automatically.
Core Components of the Registration System
Catch2 separates concerns between test metadata, test execution, and storage management through three distinct abstractions.
TestCaseInfo (Metadata Container)
Defined in src/catch2/catch_test_case_info.hpp, TestCaseInfo holds the descriptive data for each test: its name, tags, source file path, and line number. This class does not execute tests; it purely describes them, allowing the framework to filter, sort, and display tests without invoking any test logic.
ITestInvoker (Execution Interface)
Found in src/catch2/catch_test_case.hpp, ITestInvoker is an abstract base class defining the invoke() method. Concrete implementations store the test's lambda or function pointer and execute the actual test body when called. This separation allows Catch2 to hold millions of test metadata objects while deferring the instantiation of heavy test state until execution time.
TestRegistry (Central Repository)
The concrete implementation in src/catch2/internal/catch_test_case_registry_impl.hpp (and .cpp) serves as the singleton storage for all registered tests. The TestRegistry::registerTest() method receives unique pointers to both TestCaseInfo and ITestInvoker, storing them in owning containers (m_owned_test_infos and m_invokers). It creates corresponding TestCaseHandle objects—lightweight views holding raw pointers to the stored data—and maintains m_viewed_test_infos for fast iteration.
Step-by-Step Registration Flow
The Catch2 test case registration process follows a precise sequence from macro expansion to storage:
-
Macro Expansion: The user writes
TEST_CASE("My test", "[tag]") { /* body */ }, which expands toINTERNAL_CATCH_TESTCASE( __COUNTER__, "My test", "[tag]" ). -
Static Object Creation:
INTERNAL_CATCH_TESTCASEinstantiates a staticCatch::detail::AutoRegobject with a compiler-generated unique name. -
Constructor Execution: Before
main()runs, theAutoRegconstructor builds aTestCaseInfocontaining the test name, tags, and source location (__FILE__,__LINE__). -
Invoker Creation: It simultaneously creates an
ITestInvokerthat captures the test body lambda. -
Global Registration: The constructor calls
getRegistryHub().getTestCaseRegistry().registerTest( std::move(info), std::move(invoker) ). -
Storage:
TestRegistry::registerTestmoves the objects into internal storage and creates aTestCaseHandlelinking them. -
Sorting Preparation: When
Catch::Sessionstarts, it requestsgetAllTestsSorted(config)from the registry, which sorts tests (by declaration order, name, or randomization) and caches the result. -
Execution: During the test run, each handle's
invoke()method calls the storedITestInvoker, which finally executes the user-provided test body.
Under the hood, the macro expansion resembles this simplified structure:
namespace Catch {
namespace detail {
static AutoReg const& INTERNAL_CATCH_UNIQUE_NAME(auto_reg) = []{
auto info = std::make_unique<TestCaseInfo>(
"addition works", "[math]", __FILE__, __LINE__ );
auto invoker = std::make_unique<ITestInvoker>( []{
REQUIRE( 1 + 1 == 2 );
} );
getRegistryHub().getTestCaseRegistry()
.registerTest( std::move(info), std::move(invoker) );
return INTERNAL_CATCH_UNIQUE_NAME(auto_reg);
}();
}
}
RegistryHub and Singleton Management
Global access to the test registry is coordinated through RegistryHub, defined in src/catch2/interfaces/catch_interfaces_registry_hub.hpp. This singleton pattern ensures a single, well-ordered source of truth for test case data across all translation units. The getRegistryHub() function provides access to various registries (test cases, listeners, reporters) with lifetime guarantees spanning the entire program execution.
This design prevents the static initialization order fiasco by ensuring the registry exists before any AutoReg objects attempt registration, while also providing a clean interface for the Session class to query available tests.
From Registration to Execution
Once registration completes during static initialization, the TestRegistry maintains three key storage vectors:
m_owned_test_infos: Owns theTestCaseInfoobjectsm_invokers: Owns theITestInvokerobjectsm_viewed_test_infos: Provides lightweightTestCaseHandleviews for iteration
When Catch::Session initiates a test run, it queries the registry via getAllTestsSorted(config). The registry applies the configured sorting strategy—declaration order, lexicographic, or randomized—and returns a vector of TestCaseHandles. Each handle acts as a pointer pair to the metadata and invoker, allowing the session to filter by tags, apply sharding for parallel execution, and invoke tests without re-scanning source code.
Summary
- Static initialization enables automatic test discovery without explicit registration calls in user code.
- The
TEST_CASEmacro incatch_test_macros.hppgenerates staticAutoRegobjects that execute registration constructors beforemain(). - Separation of concerns:
TestCaseInfostores metadata,ITestInvokerhandles execution, andTestRegistrymanages ownership. - Registration occurs through
TestRegistry::registerTest(), which stores objects inm_owned_test_infosandm_invokerswhile exposing lightweightTestCaseHandleviews. RegistryHubprovides global singleton access to the registry across translation units.- The framework sorts and caches test handles at runtime via
getAllTestsSorted(), supporting filtering, tagging, and randomized execution.
Frequently Asked Questions
When does Catch2 actually register test cases?
Catch2 registers test cases during the static initialization phase of C++ program startup, before main() executes. When you use the TEST_CASE macro, it creates a static Catch::detail::AutoReg object whose constructor immediately registers the test with the global TestRegistry. This ensures all tests are discovered automatically without runtime scanning or explicit registration calls.
How does Catch2 avoid the static initialization order fiasco?
Catch2 uses the RegistryHub singleton pattern implemented in catch_interfaces_registry_hub.hpp. The getRegistryHub() function ensures the registry singleton is initialized on first access, and because AutoReg constructors call this function during static initialization, the registry is guaranteed to exist before any test registration occurs. This lazy initialization pattern provides a well-defined order of construction across translation units.
What is the difference between TestCaseInfo and ITestInvoker?
TestCaseInfo (defined in catch_test_case_info.hpp) is a value object containing metadata—test name, tags, source location, and line number—while ITestInvoker (defined in catch_test_case.hpp) is an abstract interface with an invoke() method that actually executes the test body. This separation allows Catch2 to filter, sort, and display thousands of tests efficiently without constructing heavy test fixtures, deferring actual test execution until the invoker is explicitly called during the test run.
Can I register test cases manually without the TEST_CASE macro?
Yes, though it requires interacting with the internal API directly. You can create a TestCaseInfo object and an ITestInvoker implementation (or use the provided lambda-based invokers), then call getRegistryHub().getTestCaseRegistry().registerTest() with these objects. However, this bypasses the automatic static initialization benefits and requires careful management of object lifetimes, which is why the macro-based approach is strongly recommended.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →