21 KiB
Trompeloeil
trompe l'oeil noun (Concise Encyclopedia)
Style of representation in which a painted object is intended to deceive the viewer into believing it is the object itself...
What is it?
A header only mocking framework for C++14.
See this introduction to get up to speed.
Follow up with the post on sequencing for examples on how to restrict or relax allowed sequences of matching calls.
If you want to contribute, read ACCU overload 125 to learn the internals.
Contents
Example usage
#include <trompeloeil.hpp>
class I
{
public:
I(const char*);
virtual ~I() {}
virtual bool foo(int, std::string& s) = 0;
virtual bool bar(int) = 0;
virtual bool bar(std::string) = 0;
};
class CUT
{
public:
CUT(I* i);
std::string work(int n);
};
class MI : public I
{
public:
using I::ID
MAKE_MOCK2(foo, bool(int, std::string&));
MAKE_MOCK1(bar, bool(int));
MAKE_MOCK1(bar, bool(std::string));
};
TEST(work_returns_the_string_obtained_from_I_foo_and_calls_I_bar)
{
trompeloeil::stream_tracer log(std::cout);
using trompeloeil::_; // wild card for matching any value
using trompeloeil::gt; // greater-than match
auto raw_i = new trompeloeil::deathwatched<MI>("word");
CUT out(raw_i);
trompeloeil::sequence seq1, seq2;
int local_var;
{
REQUIRE_CALL(*raw_i, bar(ANY(int)))
.LR_SIDE_EFFECT(local_var = _1)
.RETURN(_1 > 0)
.IN_SEQUENCE(seq1)
.TIMES(AT_LEAST(1));
FORBID_CALL(*raw_i, bar(0));
REQUIRE_CALL(*raw_i, bar("word"))
.RETURN(true)
.IN_SEQUENCE(seq2);
REQUIRE_CALL(*raw_i, foo(gt(2), _))
.WITH(_2 == "")
.IN_SEQUENCE(seq1, seq2)
.SIDE_EFFECT(_2 = "cat")
.RETURN(true);
REQUIRE_DESTRUCTION(*raw_i);
auto s = out.work(3);
ASSERT_TRUE(s == "cat");
}
}
Limitations
-
Function templates cannot be mocked
-
WAY too many macros... but I think we'll have to make do with most of them
-
Quirk! Matching a move-only type passed by value or by rvalue-refenence requires a bit of trickery in the expectation. Use a wild card in the
REQUIRE_CALL
(), and match in.WITH()
or.LR_WITH()
using alternative means. For example, astd::unique_ptr<T>
passed by value can be matched with the raw pointer value in.WITH()
.
How to use
The example above shows most currently supported functionality.
Types and Templates
trompeloeil::deathwatched<T>
Template used when monitoring the lifetime of a mock object. If a
deathwatched
object is destroyed before a REQUIRE_DESTRUCTION
is active, an error is reported. Copying or moving a deathwatched
object
does not transfer a death expectation.
NOTE! T
must have a virtual destructor.
trompeloeil::sequence
Type of sequence objects, used to impose an order of matching invocations of
REQUIRE_CALL
instances. Several sequence objects can be used to denote
parallel sequences, and several sequence objects can be joined in one
REQUIRE_CALL
.
trompeloeil::wildcard
The type of the wild card object trompeloeil::_
. You typically never see
the type itself.
trompeloeil::expectation
Base type of a named expectation object, as created by NAMED_REQUIRE_CALL
,
NAMED_FORBID_CALL
and NAMED_ALLOW_CALL
.
trompeloeil::stream_tracer
An object that traces matching calls to the std::ostream
instance provided in
the constructor. Mostly useful with ALLOW_CALL
when doing exploratory
testing of legacy code.
trompeloel::tracer
Base class for tracers. Override the member function
void trace(const char* file, unsigned long line, const std::string& call)
in your
own trace class if a trompeloeil::stream_tracer
is not right for you.
Macros
If the generic macro names conflicts with others in your sources, define the
macro TROMPELOEIL_LONG_MACROS
before inclusion of trompeloeil.hpp
. It
prefixes all macros listed below with TROMPELOEIL_
.
ANY
( type )
Typed wild card to disambiguate overloaded functions on type when the exact
value is unimportant.
AT_LEAST
( number )
Used in .TIMES
() to set the range number..infinity. number must be
constexpr
.
AT_MOST
( number )
Used in .TIMES
() to set the range 0..number. number must be
constexpr
.
MAKE_MOCKn
( name, signature )
Make a mock implementation of the member function named name. It is a good
idea for this to implement a pure virtual function from an interface, but
it is not a requirement. If no virtual member function from a base class
matches the signature, a new member function is made.
MAKE_CONST_MOCKn
( method_name, signature )
Same as MAKE_MOCKn
( method_name, signature ) for const
member functions.
REQUIRE_CALL
( mock_object, func_name(parameter_list))
Set up an expectation that the member function func_name is called on the
object mock_object. The parameter list may include exact match values, the
wild card trompeloeil::_
, or the ANY
( type ) wildcard. The
expectation has scope lifetime and must be met by the time the scope ends.
When there are several expectations active on the same object and method, they
are tried in the reversed order they were defined, i.e. last defined is tried
first, until a match is found. This allows you to specify a wide default early,
and narrow specializations in short scopes.
NAMED_REQUIRE_CALL
( mock_object, func_name(parameter_list))
Same as REQUIRE_CALL
, except it instantiates a
std::unique_ptr<trompeloeil::expectation> which you can bind to a
variable.
NOTE! Any named local objects referenced in attached .LR_WITH()
,
.LR_SIDE_EFFECT()
, .LR_RETURN()
and .LR_THROW()
are captured by
reference, so lifetime management is important.
REQUIRE_DESTRUCTION
( mock_object )
Makes it legal for a deathwatched
mock_object to be destroyed. An erro
is reported if mock_object is still alive at end of scope.
NAMED_REQUIRE_DESTRUCTION
( mock_object )
Same as REQUIRE_DESTRUCTION
, except it instantiates a
std::unique_ptr<trompeloeil::lifetime_monitor> which you can bind to a
variable.
.WITH
( expr )
Add further conditions for a REQUIRE_CALL
, typically used when the
wild card trompeloeil::_
has been used. expr can refer to parameters in the
call with their positional names _1
, _2
, etc. Even if the
function signature has parameter as non-const
references, they are
immutable in this context. Several .WITH
clauses can be added to a single
REQUIRE_CALL
. Named local objects accessed here refers to a copy.
.LR_WITH
( expr )
Same as .WITH
, except that named local objects are accessed by reference.
.SIDE_EFFECT
( expr )
Code to cause side effects. expr is only evaluated when all .WITH
clauses are matched. expr may refer to parameters in the call with their
positional names _1
, _2
, etc. This code may alter out-parameters.
Several .SIDE_EFFECT
clauses can be added to a single REQUIRE_CALL
.
Named local objects accessed here refers to a copy.
.LR_SIDE_EFFECT
( expr )
Same as .SIDE_EFFECT
, except that named local objects are accessed by
reference.
.RETURN
( expr )
Set the return value after having evaluated every .SIDE_EFFECT
. For
void
functions .RETURN
is illegal. For non-void
functions
.RETURN
is required exactly once. expr may refer to parameters in the
call with their positional names _1
, _2
, etc. This code may alter
out-parameters. If you need to return an lvalue reference, use std::ref()
.
It is not legal to combine both .THROW
and .RETURN
.
Named local objects accessed here refers to a copy.
.LR_RETURN
( expr )
Same as .RETURN
, except that named local objects are accessed by
reference.
.THROW
( expr )
Throw after having evaluated every .SIDE_EFFECT
. expr may refer to
parameters in the call with their positional names _1
, _2
, etc. This code
may alter out-parameters. It is not legal to combine both .THROW
and
.RETURN
. Named local objects accessed here refers to a copy.
.LR_THROW
( expr )
Same as .THROW
, except that named local objects are accessed by
reference.
.TIMES
( limit )
Set the number of times the call is allowed. limits must be constexpr
.
limits may be one number specifying an exact amount, or two numbers
specifying a range. A lower limit of 0
means the call is not required.
By default a REQUIRE_CALL
is expected to be tripped exactly once.
.TIMES
may only be used once for a REQUIRE_CALL
.IN_SEQUENCE
( seq... )
Where seq... is one or more instances of trompeloeil::sequence
. Impose an
order in which .REQUIRE_CALL
must match. Several sequences can be parallel
and interleaved. A sequence for a .REQUIRE_CALL
is no longer monitored
once the lower limit from .TIMES
is reached.
ALLOW_CALL
( mock_object, method_name(parameter_list))
Same as REQUIRE_CALL
().TIMES
( 0, infinity ). Matches any
number of times, but is not required to match. (actually the limit is
0..~0ULL, but that is for all practical purposes "infinity")
NAMED_ALLOW_CALL
( mock_object, method_name(parameter_list))
Same as NAMED_REQUIRE_CALL
().TIMES
( 0, infinity ).
Matches any number of times, but is not required to match. (actually the limit
is 0..~0ULL, but that is for all practical purposes "infinity").
NOTE! Any named local objects referenced in attached .LR_WITH()
,
.LR_SIDE_EFFECT()
, .LR_RETURN()
and .LR_THROW()
are captured
by reference, so lifetime management is important.
FORBID_CALL
( mock_object, method_name(parameter_list))
Same as REQUIRE_CALL
().TIMES
( 0 ), making any matching call
an error. No .RETURN
() is needed for non-void functions.
NAMED_FORBID_CALL
( mock_object, method_name(parameter_list))
Same as NAMED_REQUIRE_CALL
().TIMES
( 0 ), making any matching
call an error. No .RETURN
() is needed for non-void functions.
NOTE! Any named local objects referenced in attached .LR_WITH()
are
captured by reference, so lifetime management is important.
Matchers
In any expectation (REQUIRE_CALL
, ALLOW_CALL
, or FORBID_CALL
,)
you add the parameter values that signifies a matching call. Sometimes a
simple equality check is not enough, though. You can then choose to eiter
use WITH
(or LR_WITH
) clauses or matchers.
The matchers supplied by trompeloeil
are:
trompeloeil::ne
( value
)
Match parameters that are not equal to value
. The comparison used is
param != value
.
trompeloeil::gt
( value
)
Match parameters that are greater than value
. The comparison used is
param > value
.
trompeoleil::ge
( value
)
Match parameters that are greater than or equal to value
. The comparison used
is param >= value
.
tropmeloeil::lt
( value
)
Match parameters that are less than value
. The comparison used is
param < value
.
trompeloeil::le
( value
)
Match parameters that are less than or equal to value
. The comparison used
is param <= value
.
A matcher cannot operate on several parameters together. For more complex
matching conditions, WITH
or LR_WITH
clauses are required.
Also see writing custom matchers for extending your tests with your own matchers.
Printing values
By default trompeloeil
prints any values in violation reports using
the defined output stream operator<<
for the type. If none is available,
the objects memory region is printed as a hex-dump.
Should you want to provide a special print function for use in tests,
define a function void print(std::ostream&, const TYPE&)
in namespace
trompeloeil
for your type.
Report to test frameworks
By default trompeloeil
reports all violations as exceptions. This
works with most test frameworks, but since the error of not having matched
a REQUIRE_CALL
is not discovered until the match object goes out of
scope and is destroyed, this error will call std::terminate()
, which
is typically not what you want.
There is a function
trompeloeil::set_reporter(std::function<void(trompeloeil::severity,
char const *file,
unsigned long line,
const std::string& msg)>)
which can be used to control the reporting. trompeloeil::severity
is an enum
with the values fatal
and nonfatal
. Severity is nonfatal
when called
from the destructor of a REQUIRE_CALL
object due to unfulfilled
expectations.
NOTE! There are some violation that cannot be attributed to a source code
location. An example is an unexpected call to a mock function for which there
are no expectations. In these cases file
will be an empty string and
line
== 0.
Some examples for popular C++ unit test frameworks are:
Catch!
trompeloeil::set_reporter([](::trompeloeil::severity s,
char const *file,
unsigned long line,
const std::string& msg)
{
std::ostringstream os;
if (line) os << file << ':' << line << '\n';
os << msg;
if (s == ::trompeloeil::severity::fatal)
{
FAIL(os.str());
}
CHECK(os.str() == "");
});
crpcut
trompeloeil::set_reporter([](::trompeloeil::severity,
char const *file,
unsigned long line,
const std::string& msg)
{
std::ostringstream os;
os << file << ':' << line;
auto loc = os.str();
auto location = line == 0U
? ::crpcut::crpcut_test_monitor::current_test()->get_location()
: ::crpcut::datatypes::fixed_string::make(loc.c_str(), loc.length());
::crpcut::comm::report(::crpcut::comm::exit_fail,
std::ostringstream(msg),
location);
});
gtest
trompeloeil::set_reporter([](trompeloeil::severity s,
char const *file,
unsigned long line,
const std::string& msg)
{
if (s == trompeloeil::severity::fatal)
{
std::ostringstream os;
if (line != 0U)
{
os << file << ':' << line << '\n';
}
throw trompeloeil::expectation_violation(os.str() + msg);
}
ADD_FAILURE_AT(file, line) << msg;
});
boost Unit Test Framework
using trompeloeil::severity;
trompeloeil::set_reporter([](severity s,
char const *file,
unsigned long line,
const std::string& msg)
{
std::ostringstream os;
if (line != 0U) os << file << ':' << line << '\n';
auto text = os.str() + msg;
if (s == severity::fatal)
BOOST_FAIL(text);
else
BOOST_ERROR(text);
});
Writing custom matchers
Writing a matcher for your own data types is easy.
All matchers need to
- inherit from
trompeloeil::matcher
ortrompeloeil::typed_matcher<T>
- implement a
bool matches(parameter_value) const
member function - implement an output stream insertion operator
Typed matcher
The simplest matcher is a typed matcher. As an example of a typed matcher, an
any_of
matcher is shown, mimicing the behaviour of the standard library
algorithm
std::any_of
,
allowing a parameter to match any of a set of values.
For templated matchers, it is often convenient to provide a function that creates the
matcher object. Below is the code for any_of_t<T>
, which is the matcher created by
the any_of(std::initialize_list<T>)
function template.
template <typename T>
class any_of_t : public trompeloeil::typed_matcher<T>
{
public:
any_of_t(std::initializer_list<T> elements)
: alternatives(std::begin(elements), std::end(elements))
{
}
bool matches(T const& t) const
{
return std::any_of(std::begin(alternatives), std::end(alternatives),
[&t](T const& val) { return t == val; });
}
friend std::ostream& operator<<(std::ostream& os, any_of_t<T> const& t)
{
os << " matching any_of({";
char const* prefix=" ";
for (auto& n : t.alternatives)
{
os << prefix << n;
prefix = ", ";
}
return os << " })";
}
private:
std::vector<T> alternatives;
};
template <typename T>
auto any_of(std::initializer_list<T> elements)
{
return any_of_t<T>(elements);
}
The matches
member function at accepts the parameter and returns
true if the value is in the specified set, in this case if it is any
of the values stored in the elements
vector.
The output stream insertion operator is only called if a failure is reported. The report will then look like:
No match for call of obj.foo with signature x(y) with.
param _1 = Z
Troid obj.foo(any_of({x,y,z}) at file.cpp:8
Expected _1 XXX
Where XXX
is the output from the stream insertion operator.
Duck-typed matcher
A duck-typed matcher accepts any type that matches a required set of operations. Duck-typed matchers have a type conversion operator that selects which types it can operate on. The conversion operator is never implemented, but the signature must be available since it is used at compile time to select overload.
An example of a duck-typed matcher is a not_empty
matcher, requiring
that a .empty()
member function of the parameter returns false.
First the restricting SFINAE
predicate used to match only types that has a .empty()
member function.
template <typename T>
class has_empty
{
struct no;
static no func(...);
template <typename U>
static auto func(U const* u) -> decltype(u->empty());
public:
static const bool value = !std::is_same<no, decltype(func(std::declval<T*>()))>::value;
};
Here has_empty<T>::value
is true only for types T
that has a .empty()
member function callable on const objects.
class not_empty : public trompeloeil::matcher
{
public:
template <typename T,
typename = typename std::enable_if<has_empty<T>::value>::type>
operator T() const; //1
template <typename T>
bool matches(T const& t) const //2
{
return !t.empty();
}
friend std::ostream& operator<<(std::ostream& os, not_empty const&)
{
return os << " is not empty";
}
};
At //1 the type conversion operator selects for types that has a
.empty()
member function.
std::enable_if<>
ensures that mismatching types generates a compilation error at the site of
use (REQUIRE_CALL()
, ALLOW_CALL()
or FORBID_CALL()
.)
The matches(T const&)
member function at //2 becomes very simple. It
does not need the SFINAE
std::enable_if<>
to select
valid types, since a type mismatch gives a compilation error on the
type conversion operator at //1.
The output stream insertion operator is neither more or less tricky than with typed matchers. Making violation reports readable may require some thought, however.
Compiler compatibility
Trompeloeil is known to work with:
- GCC 4.9, 5.1
- Clang 3.5, 3.6, 3.7
- VisualStudio 2015