cpp26-reflection · part 15

Auto-generated mocks from interfaces

· upd 2026-10-05· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak· discuss

Correction, 5 October 2026. This post says that reflection with define_aggregate can generate a mock class that overrides an interface’s virtual functions. It cannot. define_aggregate adds non-static data members and nothing else; P2996R13 section 4.4.19 mentions extending it to member functions as work it does not do. I checked this on GCC 16.2.0. A base-class option does not exist (could not convert '{"base", true}' from '<brace-enclosed initializer list>' to 'std::meta::data_member_options'), and a member function is refused ('what()': 'reflection does not represent a type'). The Compiler Explorer demo at the end does not show a generated mock either: its MockClock is written by hand, and reflection only supplies the keys used to count calls. The rows of the comparison table that rely on a generated virtual mock, and the claim that this is a real class with a real vtable, do not hold. The fallback with a struct of lambdas is not an IClock, so it fits only code that is generic over the interface. That generic case does work today, with callable data members, and a later post shows it. Generating the overrides for a virtual interface needs token injection (P3294), which is C++29 work and is not in any compiler yet. The text below is left as published, so you can read it against this note.

GoogleTest’s gmock:

class IClock {
public:
    virtual ~IClock() = default;
    virtual std::chrono::seconds now() = 0;
    virtual std::string timezone() = 0;
};

class MockClock : public IClock {
public:
    MOCK_METHOD(std::chrono::seconds, now, (), (override));
    MOCK_METHOD(std::string, timezone, (), (override));
};

Every method you mock is a MOCK_METHOD line mirroring the interface. Change the interface, update the mock.

Java’s Mockito and C#’s Moq don’t need this. They reflect the interface and generate the mock at runtime. Reflection + define_aggregate brings the same ergonomics to C++, at compile time.

The goal

class IClock {
public:
    virtual ~IClock() = default;
    virtual std::chrono::seconds now() = 0;
    virtual std::string timezone() = 0;
};

// In a test:
auto mock = mocks::make<IClock>();
mock.set_return<&IClock::now>(std::chrono::seconds{1000});
mock.set_return<&IClock::timezone>(std::string{"Europe/Warsaw"});

system_under_test s{mock};
s.do_thing();

EXPECT_EQ(mock.call_count<&IClock::now>(), 3);

No MockClock class. No MOCK_METHOD lines. The library synthesises the mock from the interface at compile time.

The reflection walk

Given IClock, we reflect:

  • Each virtual member function (via members_of(^^IClock) filtered by is_function + is_virtual).
  • Each function’s signature (via parameters_of + return_type_of).
  • Override each with a stub that records the call and returns a programmed value.

The challenge: we can’t emit new class syntax from within an expression. We need define_aggregate (or define_class, see post 16) which synthesises a type from a reflection spec.

namespace mocks {

template <typename Interface>
consteval auto build_mock_spec() {
    constexpr auto ctx = std::meta::access_context::unchecked();
    std::vector<std::meta::info> members;

    // Inherit from Interface (so upcasting works)
    members.push_back(std::meta::data_member_spec(
        ^^Interface, { .name = "base", .is_base = true }));

    // For each virtual function, emit an override
    for (auto fn : std::meta::members_of(^^Interface, ctx)) {
        if (!std::meta::is_function(fn) || !std::meta::is_virtual(fn)) continue;
        members.push_back(synth_override(fn));
    }
    // Plus tracking state: call counts, programmed returns
    members.push_back(std::meta::data_member_spec(
        ^^call_tracker<Interface>, { .name = "tracker_" }));

    return std::meta::define_aggregate(^^mock_t<Interface>, members);
}

template <typename Interface>
using mock = [: build_mock_spec<Interface>() :];

}  // namespace mocks

define_aggregate creates a new type on the fly, with the shape we specify. Splicing it back gives us a real, instantiable C++ type.

Recording calls

Each synthesised override looks like:

// for std::chrono::seconds now()
std::chrono::seconds now() override {
    tracker_.record<&IClock::now>();
    return tracker_.get_return<&IClock::now, std::chrono::seconds>();
}

The tracker_ field is a call_tracker<IClock> that:

  • Stores a map from method pointers to call counts.
  • Stores programmed return values keyed by method.
  • Returns the programmed value (or default-constructed fallback).

Setting return values

template <auto Method, typename R>
void set_return(R value) {
    returns_[key_for<Method>()] = std::move(value);
}

key_for<Method>() is a consteval function that turns a member-function pointer into a stable hash. Implementation-defined; we use the std::meta::info of the function as the key, since info values are constexpr-comparable.

Verifying interactions

mock.verify<&IClock::now>().called_times(3);
mock.verify<&IClock::timezone>().never();
mock.verify<&IClock::now>().called_with_args(...);   // needs arg capture; see below

Mockito-style fluent verification builds on the call tracker.

Capturing arguments

For methods with parameters:

virtual void log(std::string_view message, LogLevel level) = 0;

// Mock override synthesises:
void log(std::string_view message, LogLevel level) override {
    tracker_.record<&ILogger::log>(message, level);
}

// Verification:
auto calls = mock.calls<&ILogger::log>();
EXPECT_EQ(calls.size(), 3);
EXPECT_EQ(std::get<0>(calls[0]), "started");

The tracker stores a std::vector<std::tuple<Args...>> per method, one entry per call. Argument types come from reflection on the function’s parameters.

Comparison

gmock Mockito (Java) Moq (C#) reflect_mocks (C++26)
Mock creation Subclass + MOCK_METHOD/method mock(IClass.class) runtime proxy Mock<IClass>() runtime proxy mocks::make<IClass>() compile-time synth
Per-method boilerplate 1 line per method none none none
Method change propagation Manual update to mock Automatic Automatic Automatic
Cost Runtime vtable indirection Runtime proxy + reflection Runtime proxy + IL emit Runtime vtable (same as gmock)
Cyclomatic complexity in mock Per-method Zero Zero Zero

The C++26 version matches the ergonomics of Mockito/Moq at the call site, without paying Mockito/Moq’s runtime proxy cost. The mock is a real C++ class; calls go through a real vtable.

Caveat: define_aggregate is experimental

P2996 includes define_aggregate as a reflection capability, but it’s flagged experimental in most implementations at time of writing. Some features (synthesising class hierarchies with virtual overrides) are still in flight. The library above is the target architecture; early implementations may need to work around gaps.

Fallback for non-define_aggregate implementations: a lightweight wrapper that takes an interface and a vtable-of-lambdas at runtime. Not as clean but fully workable today:

auto mock = mocks::make<IClock>(
    .now      = []{ return std::chrono::seconds{1000}; },
    .timezone = []{ return std::string{"Europe/Warsaw"}; }
);

Each field names a method; each value is a callable. A designated-initializer-style API that reflection still helps with (it validates that every virtual method has a binding, and synthesises the vtable under the hood).

Interaction with gmock

You can keep using gmock for complex cases (expectations, matchers, etc.) and reach for reflect_mocks when you just want a quick stub. They compose. A reflect_mocks mock can be dynamic_cast-ed, captured by a gmock::EXPECT_CALL, whatever. Reflection isn’t replacing gmock; it’s removing the boilerplate for the 80% case.

Arc 5: the advanced track

The next two posts open the advanced track.