How to Create Custom Test Fixtures in Catch2: A Complete Guide

Catch2 implements custom test fixtures through a family of macros that generate unique derived classes from your fixture class, offering three distinct lifetime options: per-section isolation with TEST_CASE_METHOD, direct member function testing with METHOD_AS_TEST_CASE, and persistent state with TEST_CASE_PERSISTENT_FIXTURE.

Creating custom test fixtures in Catch2 enables reusable setup logic and consistent test environments across multiple test cases. The catchorg/Catch2 framework achieves this through a sophisticated macro expansion system defined in the internal registry headers. By selecting the appropriate fixture pattern, you can control whether each test section receives fresh state or shares expensive resources.

How Catch2 Implements Fixture Classes

The fixture mechanism relies on macro-generated inheritance rather than virtual functions or templates alone.

The Internal Macro Expansion

In src/catch2/internal/catch_test_registry.hpp, the INTERNAL_CATCH_TEST_CASE_METHOD macro defines the core machinery. The public interface in src/catch2/catch_test_macros.hpp exposes TEST_CASE_METHOD, which expands approximately to:

class CATCH2_INTERNAL_TEST_123 : public Fixture {
    void test() { /* test body */ }
};
static auto const& CATCH2_REG_123 = registerTest(/* ... */);

This generated class inherits from your fixture, allowing access to protected members and methods. The framework instantiates this derived class for every partial run—meaning each leaf SECTION—ensuring test isolation.

Three Fixture Lifetime Patterns

Catch2 provides three distinct macros for custom test fixtures, each controlling how often the fixture class is instantiated.

TEST_CASE_METHOD (Fresh Instance Per Section)

TEST_CASE_METHOD creates a new fixture instance for every leaf SECTION. This provides maximum isolation and is ideal for database connections or file handles that must start clean.

class DbFixture {
    DBConnection conn;
public:
    DbFixture() : conn(DBConnection::create("test.db")) {}
    int nextId() { static int id = 0; return ++id; }
};

TEST_CASE_METHOD(DbFixture, "Insert without name", "[db]") {
    REQUIRE_THROWS(conn.execute("INSERT INTO users VALUES (?, '')", nextId()));
}

TEST_CASE_METHOD(DbFixture, "Insert normal", "[db]") {
    REQUIRE_NOTHROW(conn.execute("INSERT INTO users VALUES (?, 'John')", nextId()));
}

Each test case above receives its own DbFixture instance, preventing state leakage between assertions.

METHOD_AS_TEST_CASE (Member Function as Test)

METHOD_AS_TEST_CASE registers an existing member function as a standalone test, creating a new fixture instance specifically for that member function. Use this when you have class methods that naturally represent test cases.

class SimpleFixture {
    std::string s = "hello";
public:
    void checkString() { REQUIRE(s == "hello"); }
};

METHOD_AS_TEST_CASE(SimpleFixture::checkString,
                    "Check string via member function", "[simple]");

The framework instantiates SimpleFixture, calls checkString(), then destroys the instance.

TEST_CASE_PERSISTENT_FIXTURE (Single Instance Per Test Case)

Added in Catch2 3.7.0, TEST_CASE_PERSISTENT_FIXTURE creates one fixture instance for the entire test case, reusing it across all leaf SECTIONs. This avoids expensive setup costs when isolation is not required.

struct ExpensiveSetup {
    ExpensiveSetup()  { std::this_thread::sleep_for(std::chrono::seconds(2)); }
    ~ExpensiveSetup() { std::this_thread::sleep_for(std::chrono::seconds(1)); }
    int value() const { return 42; }
};

struct MyFixture {
    mutable int counter = 0;
    ExpensiveSetup heavy;
};

TEST_CASE_PERSISTENT_FIXTURE(MyFixture, "Persistent fixture demo") {
    const int cur = counter++;   // mutable allows mutation
    SECTION("first") { REQUIRE(heavy.value() == 42); REQUIRE(cur == 0); }
    SECTION("second") { REQUIRE(cur == 1); }
}

The heavy member is constructed once, and counter persists across sections, allowing you to verify execution order.

Templated Fixtures for Generic Testing

For type-parameterized tests, Catch2 provides template fixture macros defined in src/catch2/internal/catch_template_test_registry.hpp and exposed via src/catch2/catch_template_test_macros.hpp.

TEMPLATE_TEST_CASE_METHOD and Variants

These macros accept a TestType template parameter that is substituted for each type in the provided list. The fixture logic remains identical while testing different concrete types.

template <typename T>
struct VecFixture {
    std::vector<T> data{ T(1), T(2), T(3) };
};

TEMPLATE_TEST_CASE_METHOD(VecFixture, "Vector size test",
                          "[template][vector]", int, double) {
    REQUIRE(TestType::data.size() == 3);
}

The TestType alias refers to the specific instantiation of VecFixture for int or double during each partial run.

Summary

  • Fixture Generation: Catch2 creates unique derived classes via INTERNAL_CATCH_TEST_CASE_METHOD in src/catch2/internal/catch_test_registry.hpp, inheriting from your custom fixture class.
  • Per-Section Isolation: Use TEST_CASE_METHOD when you need fresh fixture state for every leaf SECTION.
  • Member Function Tests: Use METHOD_AS_TEST_CASE to expose existing class methods as standalone test cases with automatic fixture instantiation.
  • Persistent State: Use TEST_CASE_PERSISTENT_FIXTURE (available since Catch2 3.7.0) to share one fixture instance across all sections in a test case.
  • Generic Fixtures: Use TEMPLATE_TEST_CASE_METHOD and related macros in src/catch2/catch_template_test_macros.hpp to apply the same fixture logic across multiple types.

Frequently Asked Questions

How does Catch2 generate fixture classes internally?

Catch2 uses the INTERNAL_CATCH_TEST_CASE_METHOD macro in src/catch2/internal/catch_test_registry.hpp to generate a uniquely named class (e.g., CATCH2_INTERNAL_TEST_<id>) that publicly inherits from your fixture class. This derived class wraps your test body and is registered with the framework's test registry.

What is the difference between TEST_CASE_METHOD and TEST_CASE_PERSISTENT_FIXTURE?

TEST_CASE_METHOD creates a fresh fixture instance for every leaf SECTION, ensuring complete test isolation. TEST_CASE_PERSISTENT_FIXTURE, introduced in Catch2 3.7.0, creates a single fixture instance when the test case begins and destroys it after the final section completes, allowing expensive setup to be amortized across multiple sections.

Can I use custom test fixtures with multiple data types?

Yes. Use TEMPLATE_TEST_CASE_METHOD, TEMPLATE_PRODUCT_TEST_CASE_METHOD, or TEMPLATE_LIST_TEST_CASE_METHOD defined in src/catch2/catch_template_test_macros.hpp. These macros instantiate your fixture template with each specified type, accessible via the TestType alias inside the test body.

Where are the public fixture macros defined?

The public-facing macros TEST_CASE_METHOD, METHOD_AS_TEST_CASE, and TEST_CASE_PERSISTENT_FIXTURE are defined in src/catch2/catch_test_macros.hpp. The template variants reside in src/catch2/catch_template_test_macros.hpp. Both delegate to internal implementations in the corresponding internal/catch_*_registry.hpp files.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →