Trompeloeil Mocking Framework
repository·main·Indexed 21 days ago
https://github.com/rollbear/trompeloeilA thread-safe, header-only mocking framework for C++11/14. It enables developers to define expectations, return values, and side effects for virtual functions in unit tests using macros like MAKE_MOCK, REQUIRE_CALL, and FORBID_CALL. The library supports a C++14-first API with a backported variadic API for C++11 compliance and provides adapters for integration with testing frameworks such as Catch2, gtest, doctest, and CxxTest.
What's inside Trompeloeil
- Trompeloeil is a thread-safe, header-only mocking framework for C++11/14. It is distributed under the Boost Software License 1.0 and is designed to facilitate unit testing by allowing developers to create mock objects that simulate complex interfaces and behaviors.
Modify local variables using `LR_*` variants
mainStandard clauses like
.WITH(),.SIDE_EFFECT(),.RETURN(), and.THROW()capture local variables by value. This means changes made to a variable inside these clauses will not affect the variable in the test scope.To modify a local variable in your test, you must use the 'Local Reference' variants:
LR_SIDE_EFFECT()LR_WITH()LR_RETURN()LR_THROW()
Why mock objects are not move constructible
mainBy default, Trompeloeil mock objects are not move constructible. This is a safety measure: if a mock object is moved, the actions associated with its expectations (such as
.WITH(),.SIDE_EFFECT(),.RETURN(), or.THROW()) are not moved. If these actions refer to data members within the original mock object, they would end up pointing to invalid memory in the moved object.If you explicitly need movable mocks, you can use the
trompeloeil_movable_mockclass.Understand the interaction between IN_SEQUENCE and TIMES
mainMixing
.TIMES()and.IN_SEQUENCE()can lead to unexpected behavior because sequences are observed from a sequence object that moves forward only when a step is satisfied and saturated.If you use
.TIMES(AT_LEAST(1))within a sequence, the sequence object will allow the call, but it might also allow the same call to repeat. However, once the call is satisfied, the sequence object attempts to move to the next step in the sequence. If the next step is a different function, and you call the first function again, it will trigger a sequence violation because the sequence object was expecting the next step, not a repeat of the current one.trompeloeil::sequence seq; REQUIRE_CALL(mock, foo1) .TIMES(AT_LEAST(1)) .IN_SEQUENCE(seq); REQUIRE_CALL(mock, foo2) .IN_SEQUENCE(seq); REQUIRE_CALL(mock, foo3) .IN_SEQUENCE(seq); // This sequence of calls will fail: mock.foo1(); // OK: satisfies foo1 mock.foo2(); // OK: satisfies foo2 mock.foo1(); // ERROR: sequence expected foo3, but got foo1Understand 'saturated' expectations
mainA "saturated" expectation occurs when an expectation for a call exists, but the maximum number of allowed calls (defined by
.TIMES()) has already been met. Subsequent calls to that function will trigger a violation report stating that the call "Matches saturated call requirement".Example of saturation:
test_func() { test_mock obj; REQUIRE_CALL(obj, func(trompeloeil::_)) .TIMES(AT_MOST(2)); exercise(obj); // OK: 1st call exercise(obj); // OK: 2nd call exercise(obj); // FAIL: Expectation is saturated (max 2 reached) }test_func() { test_mock obj; REQUIRE_CALL(obj, func(trompeloeil::_)) .TIMES(AT_MOST(2)); exercise(obj); // OK. Expectation is alive, no prior calls, this one is accepted exercise(obj); // OK. Expectation is alive, one prior call, this one is accepted exercise(obj); // Fail. Expectation is alive, two prior calls, this one saturated }Enable move construction for mock objects
mainBy default, mock objects are immobile. To make a mock object move-constructible, add a static constexpr boolean member to your mock class:
static constexpr bool trompeloeil_movable_mock = true;Important Considerations:
- Expectation Transfer: When a mock is moved, current expectations are transferred to the new object.
- Lambda Capture Danger: If expectations use
.WITH(),.SIDE_EFFECT(),.RETURN(), or.THROW()and capture member variables of the original mock object, the lambdas will continue to refer to the old (moved-from) object. This can lead to undefined behavior or logic errors. - Lifetime Issues: If an expectation's lifetime is tied to the moved-from object (e.g., an expectation created inside a function that returns the mock), the expectation might be destroyed before it is satisfied.
Using
NAMED_REQUIRE_CALL,NAMED_ALLOW_CALL, orNAMED_FORBID_CALLcan help make expectation lifetimes more explicit.Requires
#include <trompeloeil/mock.hpp>.class movable { public: int i = 0; static constexpr bool trompeloeil_movable_mock = true; // allow move construction MAKE_MOCK1(func, void(int)); }; test(...) { movable m{3}; auto e = NAMED_REQUIRE_CALL(m, func(_)) .LR_WITH(_1 == m.i); auto mm = transfer(std::move(m)); // Danger! e still refers to m.i. ... }Control call sequences with .IN_SEQUENCE()
mainBy default, expectations are logically parallel. If you need to enforce a specific order of calls, use
trompeloeil::sequence.- Create a
trompeloeil::sequenceobject. - Attach each expectation to that sequence using
.IN_SEQUENCE(seq).
This is useful for:
- Imposing order on logically parallel calls (e.g.,
open->write->close). - Distinguishing between multiple calls that match the same parameters (e.g., a first call that fails and a second call that succeeds).
.IN_SEQUENCE(...)can also be used withREQUIRE_DESTRUCTIONandNAMED_REQUIRE_DESTRUCTION.class FileOps { public: using handle = int; MAKE_MOCK(open, auto (const std::string&) -> handle); MAKE_MOCK(write, auto (handle, const char*, size_t) -> size_t); MAKE_MOCK(close, auto (handle) -> void); }; using trompeloeil::ne; void test() { FileOps ops; trompeloeil::sequence seq; int handle = 4711; REQUIRE_CALL(ops, open("name")) .RETURN(handle) .IN_SEQUENCE(seq); REQUIRE_CALL(ops, write(handle, ne(nullptr), ne(0))) .RETURN(_3) .IN_SEQUENCE(seq); REQUIRE_CALL(ops, close(handle)) .IN_SEQUENCE(seq); test_writes(&ops); }- Create a
Core concepts of Trompeloeil
mainTrompeloeil is a C++ mocking framework built around several key abstractions:
- Mock function: A simulated implementation of a function that allows you to define expectations on how it is called.
- Mock object: An object that contains one or more mock functions, used to simulate a complex interface or class.
- Expectation: A rule defined for a mock function that specifies which arguments are acceptable, how many times the function should be called, and what it should return or do.
- Matcher: A tool used within an expectation to validate the arguments passed to a mock function. Matchers can check for equality, ranges, specific types, or even use regular expressions.
Set expectations with ALLOW_CALL, REQUIRE_CALL, and FORBID_CALL
mainTrompeloeil uses expectations to define the behavior of mock functions. By default, all calls to mock functions are illegal and will be reported as violations. You can use three basic types of expectations:
ALLOW_CALL(...): Used for defaults. It can match any number of times.REQUIRE_CALL(...): Stricter; it defaults to matching exactly once, but you can control the match count.FORBID_CALL(...): Explicitly forbids a call. Useful when combined withALLOW_CALLorREQUIRE_CALLto prevent specific calls that would otherwise be accepted.
Scoping and Lifetime: Expectations are active until the end of their scope. If multiple expectations match a call, the last matching expectation created is used. This allows you to set a wide default in a outer scope and use narrow, temporary expectations in a local scope.
If you need to control the lifetime manually, use the named versions which return a
std::unique_ptr<trompeloeil::expectation>:NAMED_ALLOW_CALL(...)NAMED_REQUIRE_CALL(...)NAMED_FORBID_CALL(...)
class Mock { public: MAKE_MOCK(func, auto (int) -> void); MAKE_MOCK2(func, auto (const char*) -> void); }; void test() { Mock m; ALLOW_CALL(m, func(1)); // int version any number of times REQUIRE_CALL(m, func(nullptr)); // const char * version exactly once func(&m); // expectations must be met before end of scope }Thread safety and using the global lock
mainTrompeloeil is thread-safe using a global
recursive_mutexthat protects expectations. This allows expectations and mock functions to be accessed across different threads.Warning: You must ensure the mock object is not deleted while establishing an expectation or calling a mock function.
If you need to manually access the lock in your tests, use
trompeloeil::get_lock().auto lock = trompeloeil::get_lock(); // lock holds the recursive_mutex until it goes out of scopeHow sequence objects work with expectations
mainYou can use
trompeloeil::sequenceobjects to enforce a specific order of calls across different expectations. When an expectation uses.IN_SEQUENCE(seq), it must occur in the order defined by the sequence objects.If multiple expectations share the same sequence object, they must occur in the order they were defined. If an expectation is listed in multiple sequences, it must satisfy the constraints of all of them.
Greedy Behavior:
.IN_SEQUENCE()combined with.TIMES(min, max)is greedy. The expectation will remain active and match calls as long as it matches and hasn't reached its upper bound, or until a different expectation matches. This allows a single expectation to 'consume' calls before the sequence moves to the next step.class Mock { public: MAKE_MOCK1(func, void(int)); }; TEST(a_test) { Mock m; trompeloeil::sequence seq; REQUIRE_CALL(m, func(0)) .IN_SEQUENCE(seq) .TIMES(1, 5); ALLOW_CALL(m, func(0)) .IN_SEQUENCE(seq) .SIDE_EFFECT(std::cout << "extra\n"); REQUIRE_CALL(m, func(1)) .IN_SEQUENCE(seq); test_func(m); }Create duck-typed matchers with `wildcard`
mainA duck-typed matcher accepts any type that supports a specific set of operations (e.g., a
.empty()method). To implement this, usetrompeloeil::wildcardas the type argument fortrompeloeil::make_matcher<trompeloeil::wildcard>(...).Crucial Requirement: The predicate lambda must use a trailing return type specifier that utilizes the required operations. This allows SFINAE (Substitution Failure Is Not An Error) to filter out incompatible types at compile time, providing a clear error at the call site (
REQUIRE_CALL, etc.) rather than deep inside the matcher logic.Example of a
not_empty()matcher:inline auto not_empty() { return trompeloeil::make_matcher<trompeloeil::wildcard>( // predicate lambda with trailing return type for SFINAE [](auto const& value) -> decltype(!value.empty()) { return !value.empty(); }, // print lambda [](std::ostream& os) { os << " is not empty"; } ); }inline auto not_empty() { return trompeloeil::make_matcher<trompeloeil::wildcard>( [](auto const& value) -> decltype(!value.empty()) { return !value.empty(); }, [](std::ostream& os) { os << " is not empty"; } ); }