# How Catch2 Test Case Registration Works Internally: Static Initialization and Registry Architecture

> Discover how Catch2 test case registration works internally. Learn about static initialization and the registry architecture through the TEST_CASE macro.

- Repository: [Catch Org/Catch2](https://github.com/catchorg/Catch2)
- Tags: internals
- Published: 2026-07-30

---

**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`](https://github.com/catchorg/Catch2/blob/main/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`](https://github.com/catchorg/Catch2/blob/main/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`](https://github.com/catchorg/Catch2/blob/main/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`](https://github.com/catchorg/Catch2/blob/main/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:

1. **Macro Expansion**: The user writes `TEST_CASE("My test", "[tag]") { /* body */ }`, which expands to `INTERNAL_CATCH_TESTCASE( __COUNTER__, "My test", "[tag]" )`.

2. **Static Object Creation**: `INTERNAL_CATCH_TESTCASE` instantiates a static `Catch::detail::AutoReg` object with a compiler-generated unique name.

3. **Constructor Execution**: Before `main()` runs, the `AutoReg` constructor builds a `TestCaseInfo` containing the test name, tags, and source location (`__FILE__`, `__LINE__`).

4. **Invoker Creation**: It simultaneously creates an `ITestInvoker` that captures the test body lambda.

5. **Global Registration**: The constructor calls `getRegistryHub().getTestCaseRegistry().registerTest( std::move(info), std::move(invoker) )`.

6. **Storage**: `TestRegistry::registerTest` moves the objects into internal storage and creates a `TestCaseHandle` linking them.

7. **Sorting Preparation**: When `Catch::Session` starts, it requests `getAllTestsSorted(config)` from the registry, which sorts tests (by declaration order, name, or randomization) and caches the result.

8. **Execution**: During the test run, each handle's `invoke()` method calls the stored `ITestInvoker`, which finally executes the user-provided test body.

Under the hood, the macro expansion resembles this simplified structure:

```cpp
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`](https://github.com/catchorg/Catch2/blob/main/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 the `TestCaseInfo` objects
- `m_invokers`: Owns the `ITestInvoker` objects  
- `m_viewed_test_infos`: Provides lightweight `TestCaseHandle` views 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 `TestCaseHandle`s. 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_CASE` macro in [`catch_test_macros.hpp`](https://github.com/catchorg/Catch2/blob/main/catch_test_macros.hpp) generates static `AutoReg` objects that execute registration constructors before `main()`.
- **Separation of concerns**: `TestCaseInfo` stores metadata, `ITestInvoker` handles execution, and `TestRegistry` manages ownership.
- Registration occurs through `TestRegistry::registerTest()`, which stores objects in `m_owned_test_infos` and `m_invokers` while exposing lightweight `TestCaseHandle` views.
- `RegistryHub` provides 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`](https://github.com/catchorg/Catch2/blob/main/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`](https://github.com/catchorg/Catch2/blob/main/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`](https://github.com/catchorg/Catch2/blob/main/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.