Trompeloeil 模拟的简明 RAII

Concise RAII for Trompeloeil mocks

我有class这样的:

/* "Things" can be "zarked", but only when opened, and they must be closed afterwards */
class ThingInterface {
public:
   // Open the thing for exclusive use
   virtual void Open();
   // Zark the thing.
   virtual void Zark(int amount);
   // Close the thing, ready for the next zarker
   virtual void Close();
};

/* RAII zarker - opens Things before zarking, and closes them afterwards */
class Zarker {
public:
    Zarker(Thing& thing): m_thing(thing) {
        m_thing.Open();
    }

    ~Zarker() {
        m_thing.Close();
    }

    void ZarkBy(int amount) {
        m_thing.Zark(amount);
    }

private:
    Thing& m_thing;
}

然后我有一个 Trompeloeil 模拟 class 实现接口:

struct MockThing: public ThingInterface {
    MAKE_MOCK0(Open, void(), override);
    MAKE_MOCK1(Zark, void(int), override);
    MAKE_MOCK0(Close, void(), override);
};

然后我想测试 42: 的“zarking”,包括(关键)OpenClose 程序。

MockThing mockedThing;
trompeloeil::sequence sequence;

REQUIRE_CALL(mockedThing, Open())
    .IN_SEQUENCE(sequence));

REQUIRE_CALL(mockedThing, Zark(42))
    .IN_SEQUENCE(sequence));

REQUIRE_CALL(mockedThing, Close())
    .IN_SEQUENCE(sequence));

Zarker zarker(mockedThing);
zarker.ZarkBy(42);

这很好用,并证明了 Zarker 实际上会在使用后关闭其 Thing(如果不这样做,就会发生坏事)。

现在,我还有很多测试要做,我想保持 DRY 状态,避免重复对 OpenClose 调用的模拟期望。在现实生活中,这些期望实际上不仅仅是一对函数,还有其他设置和拆卸操作必须按照仔细的顺序进行,并且在每次测试中都必须重复这些操作会非常烦人。

由于这是 RAII 惯用语,因此将其用于期望似乎很自然。但是,由于 Trompeloeil 期望是有范围的,因此您必须使用 NAMED_REQUIRE_CALL 并保存 unique_ptr<trompeloeil::expectations>:

class RaiiThingAccessChecker {
public:
    /* On construction, append the setup expectation(s) */
    RaiiThingAccessChecker(
        MockThing& thing,
        trompeloeil::sequence& sequence,
        std::vector<std::unique_ptr<trompeloeil::expectation>>& expectations
    ):
        m_thing(thing),
        m_sequence(sequence),
        m_expectations(expectations) {

        m_expectations.push_back(
            NAMED_REQUIRE_CALL(m_thing, Open())
                .IN_SEQUENCE(m_sequence)
        );
    }

    /* On wrapper destruction, append the teardown expectation(s) */
    ~RaiiThingAccessChecker() {
        m_expectations.push_back(
            NAMED_REQUIRE_CALL(m_thing, Close())
                .IN_SEQUENCE(m_sequence)
        );
    }

private:
    MockThing& m_thing,
    trompeloeil::sequence& m_sequence,
    std::vector<std::unique_ptr<trompeloeil::expectation>>& m_expectations
}

那么你可以这样使用它:

MockThing mockedThing;
trompeloeil::sequence seq;
std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;

{
    RaiiThingAccessChecker checker(mockedThing, seq, expectations);

    mockExpectations.push_back(
        NAMED_REQUIRE_CALL(mockedThing, Zark(42))
            .IN_SEQUENCE(seq)
    );
}

Zarker zarker(mockedThing));
zarker.ZarkBy(42);

所以这已经足够实用了,但它似乎相当冗长 - 你必须坚持 期望指针的容器,RaiiThingAccessChecker 之外的序列 而且你还必须将它们全部传入,并在内部存储引用。

有没有更简洁的方法,或者其他惯用的方法来实现这种 “期望重用”?我有很多“模块化”的期望可以像这样建模。

我倾向于创建函数而不是 RAII class 那里:

std::unique_ptr<std::pair<MockThing, trompeloeil::sequence>>
MakeMockedThing(std::function<void(MockThing&, trompeloeil::sequence&)> inner)
{
    auto res = std::make_unique<std::pair<MockThing, trompeloeil::sequence>>();
    auto& [mock, sequence] = *res;
    REQUIRE_CALL(mock, Open()).IN_SEQUENCE(sequence));

    inner(mock, sequence);

    REQUIRE_CALL(mock, Close()).IN_SEQUENCE(sequence));
    return res;
);

然后

auto p = MakeMockedThing([](MockThing& thing, trompeloeil::sequence& sequence)
    {
        REQUIRE_CALL(thing, Zark(42)).IN_SEQUENCE(sequence));
    });
auto& [mock, sequence] = *p;
Zarker zarker(mock);
zarker.ZarkBy(42);

注意:std::pair<MockThing, trompeloeil::sequence> 可能会被模板 class 取代以获得更好的语法。