'Concise RAII for Trompeloeil mocks
I have classes like this:
/* "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;
}
I then have a Trompeloeil mock class that implements the interface:
struct MockThing: public ThingInterface {
MAKE_MOCK0(Open, void(), override);
MAKE_MOCK1(Zark, void(int), override);
MAKE_MOCK0(Close, void(), override);
};
I then wish to test a "zarking" of 42:, including the (critical) Open
and Close
procedure.
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);
This works nicely and demonstrates that a Zarker
does, in fact, close its Thing
after use (Bad Things happen if it does not).
Now, I have a lot more tests to go, and I'd like to stay DRY and avoid duplicating the mock expectations for the Open
and Close
calls. In real life, these expectations are actually not just a pair of functions, there are other setup and teardown actions that have to happen in careful order, and it'll be very annoying to have to repeat this in every test.
Since this is a RAII idiom, it seems natural to also use that for the expectations. However, because Trompeloeil expectations are scoped, you have to use NAMED_REQUIRE_CALL
and save the 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
}
Then you can use it like this:
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);
So that's functional enough, but it seems rather verbose - you have to hold
a container of expectation pointers, the sequence outside the RaiiThingAccessChecker
and you also have to pass them all in, and store references internally.
Is there a more concise way, or otherwise idiomatic way to achieve this kind of "expectation reuse"? I have a lot of "modular" expectations that can be modelled like this.
Solution 1:[1]
I would have tendency to create function instead of RAII class there:
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;
);
And then
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);
Note: std::pair<MockThing, trompeloeil::sequence>
might probably be replaced by template class to have nicer syntax.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Jarod42 |