This sample demonstrates the basics of DrMock's mock features.
- Introduction
- Setup
- Including the mock object header
- Using the mock object
- Running the tests
- DrMock API
- Details
drmock_library
anddrmock-generator
- Fine print
- Bibliography
samples/mock
│ CMakeLists.txt
│ Makefile
│
└───src
│ │ CMakeLists.txt
│ │ IWarehouse.h
│ │ Order.cpp
│ │ Order.h
│ │ Warehouse.cpp
│ │ Warehouse.h
│
└───tests
│ CMakeLists.txt
│ OrderTest.cpp
Note that this samples uses the typical CMake project structure with
three CMakeLists.txt
. src/CMakeLists.txt
manages the source files
and tests/CMakeLists.txt
the tests.
This project requires an installation of DrMock in build/install/
or a path container in the CMAKE_PREFIX_PATH
variable. If your
installation of DrMock is located elsewhere, you must change the
value of CMAKE_PREFIX_PATH
.
Let's introduce the concept of mock with an example from M. Fowler's
Mocks Aren't Stubs [1]:
At an online shop, customers fill out an Order
with the commodity
they'd like to order and the order quantity. The Order
may be filled
from a Warehouse
using fill
. But fill
will fail if the Warehouse
doesn't hold the commodity in at least the quantity requrested by the
customer. We want to test this interaction between Order
and
Warehouse
, and, assuming that the unit Warehouse
has already been
tested, will use a mock of Warehouse
for this purpose. This will help
decouple the correctness of the implementation of Order::fill
from
the correctness of the implementation of Warehouse
.
For the sake of clarity, we separte the interface and implementation of
the warehouse functionality into two classes IWarehouse
(the
interface) and Warehouse
(the implementation, which is a subclass of
IWarehouse
):
// IWarehouse.h
// ...
class IWarehouse
{
public:
virtual ~IWarehouse() = default;
virtual void add(std::string commodity, std::size_t quantity) = 0;
virtual bool remove(const std::string& commodity, std::size_t quantity) = 0;
};
(Note that remove
returns a bool
. If removing fail due to too large a
quantity being requested, this should be false
. Otherwise, true
.)
DrMock generates source code of mock implementations from C++
classes called interfaces (the class that you wish to mock, in this
case IWarehouse
).
We have some minor requirements regarding the structure of the
interface. You can review them in the DrMock
specification. This is fine print - most classes will
qualify. The most important requirements are summarized in Fine print.
Unless you encounter a problem, there is no reason to look deeper into
these details.
The code generations is done using the
drmock-generator, which is
developed in Python and available via pip
. The generator is called
at compile time to generate the source code of the mock implementation.
During runtime (for example during a test), the exact behavior of the
implementation may then be configured using the API specified in detail
below. For example:
DRTEST_TEST(success)
{
drmock::samples::Order order{"foo", 2};
auto warehouse = std::make_shared<drmock::samples::WarehouseMock>();
// Inform `warehouse` that it should expect an order of two units of
// `"foo"` and should return `true` (indicating that those units are
// available).
warehouse->mock.remove().push()
.expects("foo", 2) // Expected arguments.
.times(1) // Expect **one** call only.
.returns(true); // Return value.
order.fill(warehouse);
// Check that `remove` was called with the correct arguments.
DRTEST_ASSERT(warehouse->mock.control.verify());
// Check that the return value of `filled` is correct.
DRTEST_ASSERT(order.filled());
}
We will explain the function of this API later. If you are curious about the implementation of the API, check out the DrMock specification or the drmock-generator source code.
Note.
The requirement that the interface be abstract is removed in
DrMock 0.6. This means that the separation of interface IWarehouse
and implementation Warehouse
is not required anymore. Instead,
Warehouse
could serve both as interface and as implementation.
Nevetheless, we will proceed using IWarehouse
as interface.
To instruct drmock-generator
to create source code for a mock of
IWarehouse
, the macro drmock_library
is used.
# src/CMakeLists.txt
add_library(DrMockSampleMock SHARED
Order.cpp
Warehouse.cpp
)
drmock_library(
TARGET DrMockSampleMockMocked
HEADERS
IWarehouse.h
)
The HEADERS
parameter specifies the interface to be mocked.
The drmock_library
macro generates the source code of the mocks and
compiles them into a dynamic library whose name is specified by
TARGET
. Other parameters of drmock_library
are discussed later.
As discussed in the last chapter, we must now use the LIBS
parameter
of drmock_test
to link the test against the binary which contains the
mock code:
# tests/CMakeLists.txt
drmock_test(
LIBS
DrMockSampleMock
DrMockSampleMockMocked
TESTS
OrderTest.cpp
)
Note.
The drmock_library
macro's implementation is a wrapper of
drmock-generator
. It hides some of the parameters of
drmock-generator
which are tedious for the user to manage (type
drmock-generator --help
in the terminal to see a full list of the
parameters), but it does
so at the cost of forcing you to use CMake as build tool. If can't or
won't use CMake, you have two options. You could manage the
drmock-generator
call manually. This is similar to writing you own
Makefile instead of using a modern build tool and is not recommended.
The other is to contribute by implementing
drmock_library
for the build tool you wish to use.
By default, the mock of I*
is called *Mock
and placed in the same
namespace as the interface, but you may change this pattern if you like.
The header file of the mock object is included with #include "mock/WarehouseMock.h"
.
For every file under HEADER
, say path/to/IFoo.h
, DrMock
generates header and source files FooMock.h
and FooMock.cpp
, which
may be included using the path mock/path/to/IFoo.h
. In these files,
the mock class FooMock
is defined. The path is relative to the
current CMake source dir. Thus, calling drmock_library
from anywhere
but src/CMakeLists.txt
is bound to result in odd include paths.
Let's take a look at the header of Order
:
// Order.h
// ...
class Order
{
public:
Order(std::string commodity, std::size_t quantity);
void fill(const std::shared_ptr<IWarehouse>&);
bool filled() const;
private:
bool filled_;
std::string commodity_;
std::size_t quantity_;
};
Note that fill
takes a reference to a std::shared_ptr
to allow the
use of polymorphism. You could use virtually any type of (smart) pointer
in place of std::shared_ptr
, or even a reference if your interface is
not abstract.
Here's the straightforward implementation of the fill
method:
void
Order::fill(const std::shared_ptr<IWarehouse>& wh)
{
filled_ = wh->remove(commodity_, quantity_);
}
Thus, the order will attempt to remove the requested amount of units of
the commodity from the warehouse and set filled_
accordingly.
How do we test fill
? Let's look at the success
test of
OrderTest.cpp
(you've already seen it in the introduction):
DRTEST_TEST(success)
{
drmock::samples::Order order{"foo", 2};
auto warehouse = std::make_shared<drmock::samples::WarehouseMock>();
// Inform `warehouse` that it should expect an order of two units of
// `"foo"` and should return `true` (indicating that those units are
// available).
warehouse->mock.remove().push()
.expects("foo", 2u) // Expected arguments.
.times(1) // Expect **one** call only.
.returns(true); // Return value.
order.fill(warehouse);
// Check that `remove` was called with the correct arguments.
DRTEST_ASSERT(warehouse->mock.control.verify());
// Check that the return value of `filled` is correct.
DRTEST_ASSERT(order.filled());
}
The third line (warehouse->mock.remove().push() ...
) uses the API of
DrMock to configure the behavior of warehouse
. Every mock object
contains a public member mock
of type DRMOCK_OBJECTIWarehouse
, whose
source code is generated alongside that of WarehouseMock
. This
user handle lets the user define the expected behavior of the
mock obejct:
auto warehouse = std::make_shared<drmock::samples::WarehouseMock>();
warehouse->mock.remove().push()
.expects("foo", 2u)
.times(1)
.returns(true);
To define the behavior of remove
, we call warehouse->mock.remove()
.
This returns a drmock::Method
object which controls the behavior of
warehouse->remove
. By default, the a drmock::Method
object has an
internal queue onto which drmock::Behavior
objects may be pushed.
Here, we push one element, which instructs the mock object to expect
a call warehouse->remove("foo", 2)
, to expect it exactly one time,
and to return true
if warehouse->remove("foo", 2)
is called.
If an unexpected call is made (or too many or too few expected calls),
then the Method
object will log an error.
Note. push
, expects
, times
, returns
, etc. return a reference
to the pushed Behavior
object, allowing us to use call chaining when
configuring a mock object.
Now that the behavior of warehouse
is defined, the order for two units
of foo
is filled from the warehouse. Judging from the implementation
of fill
, this should call warehouse->remove("foo", 2)
. And, as
we defined earlier (by calling returns(true)
), removing two units of
foo
should "succeed".
Whether the expected behavior was observed or not may be verified using the following call:
DRTEST_ASSERT(warehouse->mock.remove().verify());
This is equivalent to:
DRTEST_VERIFY_MOCK(warehouse->mock.remove());
After verifying the mock, we check if the filled
method returns the
correct value:
DRTEST_ASSERT(order.filled());
Note. When verifying the mock object, the Behavior
objects are
expected to occur in the order in which they were pushed. More on that
later.
The second test runs along the same lines. The customer places an order for two units of foo, but this time a failure is simulatedd:
auto warehouse = std::make_shared<drmock::samples::WarehouseMock>();
warehouse->mock.remove().push()
.expects("foo", 2u)
.times(1)
.returns(false);
Once again, warehouse->mock
is verified, and order.filled()
is now
expected to return false
.
Note. The terminology is not identical to that used in the DrMock specification. In the specification, the user handle is simply called mock object, and the mock object is called the mock implementation.
Do make
to run the tests. The following should occur:
Start 1: OrderTest
1/1 Test #1: OrderTest ........................ Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.01 sec
Every virtual method f
(add
, remove
, ...) of IWarehouse
corresponds to a drmock::Method
object which can be obtained by
calling warehouse->mock.f()
(overloads are resolved using template
parameters, see Accessing overloads). A detailed documentation of Method
may be
found in Method.h.
The behavior of f
is controlled by the behavior of the underlying
Method
object. In fact, the implementation of WarehouseMock::f
uses
Method::call
, which simulates one function call
(for details, see the DrMock specification).
By default, the behavior of a Method
object is controlled by a
BehaviorQueue
object (the queue; in the next chapter, we will use a
StateBehavior
, instead). The queue is empty when the BehaviorQueue
is constructed. As we saw above, new Behavior
objects may be pushed
onto the queue using BehaviorQueue::push
or the convenience method
Method::push
. Both methods return a reference to the newly pushed
Behavior
.
In other words, the following code configures an instance of Behavior
on the queue of warehouse
:
warehouse->mock.remove().push(). // drmock::Behavior&
// Some config calls...
A Behavior
object (behavior) is the smallest unit used to simulate a
method.
Every behavior can expect a call with a specified input
(a set of arguments that match the parameter types), and produce a
result when called. The result can be a return value and/or a Qt
signal emit (if you're not familiar with this, ignore it), or an
exception pointer (simulating a thrown exception). Returning a value and
emitting a signal may both occur, but exceptions are exclusive.
Furthermore, every Behavior
object has a life span, which means that
it can only produce()
a fixed number of results (may be infinite).
Once the fixed number of productions is reached, the behavior no longer
persists. The results of the production, as well as the life span must
be configured by the user.
For example,
warehouse->mock.remove().push()
.expects("foo", 2u)
.times(1)
.returns(true);
pushes a new Behavior
onto the BehaviorQueue
, then configures that
Behavior
to expect the input ("foo", 2)
, to persists for exactly one
production, and to produce true
.
The available methods for configuring a Behavior
object are the
following:
Behavior& expects()
- Expect any input (this is the default)Behavior& expects(detail::Expect<Args>... args)
- Expect input that matchesargs
(Args...
are the parameters off
); thei
th element may be an instance ofArgs[i]
or an instance ofstd::shared_ptr<IMatcher<Args[i]>>
template<typename... Deriveds> Behavior& polymorphic()
- Set theDerived...
type pack for the matching handlertemplate<typename T> Behavior& return(T&& result)
- Produce return valueresult
when calledtemplate<typename E> Behavior& throws(E&& e)
- Produce exception valuee
when calledtemplate<typename E> Behavior& emits(void (Class::*signal)(SigArgs...), SigArgs&&... args)
- Produce a Qt signal emit when calledBehavior& times(unsigned int count)
- Expect exactlycount
productions (function calls)Behavior& times(unsigned int min, unsigned int max)
- Expect production count in[min, max]
Behavior& persists()
- Expect any number of productions and makethis
immortal
The default expected number of productions is 1
.
Some of the configuration calls may be combined, while others create conflicts:
-
Multiple calls to the same function are not allowed
-
returns
andemits
may be combined (the method will emit first, then return, of course) -
throws
may not be combined withreturns
oremits
For example:
behavior
.returns(123)
.emits(&Dummy::f, 456) // Ok, returns and emits are parallel.
.throws(std::logic_error{""}) // Not ok, overriding previous behavior.
.returns(456) // Not ok, overriding previous behavior.
For details, refer to Behavior.h Matching and polymorphism are described in Matching and polymorphism.
The queue controls the behavior of f
as follows. Let b
be the front
element of the queue. When f
is called with args...
, the following
occurs:
- If
b
expectsargs...
, then the result ofb
occurs (f
returns a value and/or emits signal, or throws exception) - Otherwise, the call fails (see Failure for details)
- If
b
has reached the maximum number of productions (b
is exhausted), thenb
is popped off the queue
If the queue is empty and f
is called, the call fails (see Failure).
Furthermore, BehaviorQueue
exposes an API for configuration:
void enforce_order(bool)
- When set tofalse
, the order of the queue is ignored and the entire queue is searched for a matching behavior on every calltemplate<typename... Deriveds> void polymorphic()
- Set theDeriveds...
type pack of all future behaviors and all behaviors already enqueued to the default, with the specified polymorphic type (see Matching and polymorphism for details)
You can check the correctness of a Method
object by calling
verify()
, which returns false
if any function call of f
has
failed. You can use makeFormattedErrorString()
to get a report of all
errors that have occured.
If Method::call
fails, DrMock will try to gracefully recover:
-
If the return type is default constructible, return a default constructed value.
-
If the return type is
void
, return. -
Otherwise,
std::abort
.
The DRTEST_VERIFY_MOCK
macro calls verify()
and prints
makeFormattedErrorString()
if it returns false
.
Note. A failed execution is caused by unexpected behavior of any of the components that access the mock object, or by an incorrect configuration of the queue.
See samples/example for a sample that demontrates the use of the functions discussed above.
By default, mock
contains a member control
of type
drmock::Controller
, which holds all the Method
objects. This object
provides diagnostic methods, some of which only make sense in the
context of StateBehavior
(see the next chapter).
-
You can verify all methods of a mock object at once by calling
warehouse->mock.control.verify()
or, equivalently,
DRTEST_VERIFY_MOCK(warehouse->mock);
Beware! Unconfigured methods will result in a failed test if this macro is used.
-
If any methods have failed, use
makeFormattedErrorString
to obtain a comprehensive summary of the errors that have occured
Consider the following interface:
class IBar
{
public:
virtual ~IBar() = default;
virtual int f() = 0;
virtual int f() const = 0;
virtual int f(int) = 0;
virtual int f(float, const std::vector<int>&) const = 0;
virtual int& g(std::vector<int>&) = 0;
virtual const int& g(std::vector<int>&) const = 0;
virtual float h() const = 0;
virtual float h(int, float) const = 0;
};
How do we access the methods here? We can't just do bar->mock.f()
-
DrMock wouldn't know which overload you want!
Instead, template parameters must be used:
-
If all members of the overload have the same const/reference qualifiers (but different parameters), then
bar->mock.f<Ts...>()
returns theMethod
object corresponding to the overload with parametersTs...
. For example,bar->mock.h<>()
selectsfloat h() const
, andbar->mock.h<int, float>()
selectsfloat h(int, float) const
. -
If all members of the overload have the same parameters (but different const/reference qualifiers), then use
drmock::Const
,drmock::LValueRef
anddrmock::RValueRef
as template parameters to select methods (in this order, i.e.drmock::Const
before references). For example,bar->mock.g<>()
selectsint& g(std::vector<int>&)
, andbar->mock.g<drmock::Const>()
selectsconst int& g(std::vector<int>&) const
. -
If members of the overload have varying parameters and qualifiers, then use parameter types
Ts...
followed by qualifiers types (see above) to select overloads. For example,bar->mock.f<int>()
selectsint f(int)
, andbar->mock.f<float, const std::vector<int>&, drmock::Const>()
selectsf(float, const std::vector<int>&) const
.
Below is a complete example (see BarTest.cpp):
DRTEST_TEST(params_and_qualifiers)
{
auto bar = std::make_shared<BarMock>();
bar->mock.f<>().push()
.expects()
.returns(1);
bar->mock.f<drmock::Const>().push()
.expects()
.returns(2);
bar->mock.f<int>().push()
.expects(3)
.returns(3);
bar->mock.f<float, const std::vector<int>&, drmock::Const>().push()
.expects(1.2f, {1, 2, 3})
.returns(4);
DRTEST_ASSERT_EQ(
bar->f(),
1
);
DRTEST_ASSERT_EQ(
std::const_pointer_cast<const BarMock>(bar)->f(),
2
);
DRTEST_ASSERT_EQ(
bar->f(3),
3
);
DRTEST_ASSERT_EQ(
bar->f(1.2f, {1, 2, 3}),
4
);
}
Recall that
Behavior& Behavior::expects(Expect<Args>... args)
defines a behavior to produce only when called with arguments that
match args...
. But the parameters of expects
are
Expect<Args>...
, which is a variant type. Making use of implicit
conversion, the type of the i
th element of args...
may be Args[i]
,
but it may also be a std::shared_ptr<IMatcher<Args[i]>>
.
The definition of IMatcher
is simple:
template<typename Base>
class IMatcher
{
public:
virtual ~IMatcher() = default;
virtual bool match(const Base& x) const = 0;
};
The match
method matches the object x
against some pattern. For
example, Equal implements IMatcher
and checks if x
is equal to the element that the instance of Equal
was constructed with:
Equal<int> equal_to_five{5};
DRTEST_ASSERT(equal_to_five->match(5));
DRTEST_ASSERT(not equal_to_five->match(2 + 2));
(More on the second template parameter of Equal
later.)
By default, if expects
is called with an element args
of type
Args[i]
instead of std::shared_ptr<IMatcher<Args[i]>>
in i
th
position (this is called raw input), it is wrapped in an instance of
std::shared_ptr<Equal<Args[i]>>
by the matching handler. Internally,
the behavior only works with matchers. When called, it checks if the
input in each position matches the corresponding matcher and produces
only if this is the case.
By letting us pass an std::shared_ptr<IMatcher<Args>>
, the API
allows us to specify more complex matching behavior. For example,
AlmostEqual implements typical
floating point approximation logic and may be used to define a
production for a small range of floating point numbers, compensating the
lack of absolute precision in floating point math:
b.expects("foo", drmock::almost_equal(1.0f));
This expects the arguments to be "foo"
and a floating point number
almost equal to 1.0f
.
Users may implement
their own matchers by implementing IMatcher
.
The second template parameter of Equal
allows the user to take
polymorphism into account. Consider the following situation:
class Base
{
// ...
};
class Derived : public Base
{
public:
Derived(int x) : x_{x} {}
bool operator==(const Derived& other) const
{
return x_ == other.x_;
}
// ...
private:
int x_;
// ...
};
// IFoo.h
class IFoo
{
public:
~IFoo() = default;
void func(std::shared_ptr<Base>) = 0;
};
Suppose we want to configure FooMock
to expect
ptr = std::make_shared<Derived>(1)
. This is done using
Equal<std::shared_ptr<Base>, std::shared_ptr<Derived>>
, which applies
dynamic_pointer_cast<Derived>
before matching the actual value against
the expected value:
DRTEST_TEST(polymorphic)
{
FooMock foo{};
auto ptr = std::make_shared<Derived>(1);
foo.mock.func().push()
.expects(drmock::equal<Base, Derived>(ptr));
}
To improve readability, we can use polymorphic<Deriveds...>()
to
replace the default matching handler with one that wraps raw input in
std::shared_ptr<Equal<std::shared_ptr<Base>, std::shared_ptr<Derived>>>
instead of the default
std::shared_ptr<Equal<std::shared_ptr<Base>>>
:
DRTEST_TEST(polymorphic)
{
FooMock foo{};
auto ptr = std::make_shared<Derived>(1);
foo.mock.func().push()
.polymorphic<std::shared_ptr<Derived>>()
.expects(ptr);
}
Note that this call will not change the expected value. Only the effect
of future calls to Behavior::expect
is changed. Note that
BehaviorQueue::polymorphic
sets the matching handler for previously
enqueued behaviors and future behaviors.
Polymorphism is supported for std::shared_ptr
and std::unqiue_ptr
.
However, there is a requirement on the types (B, D)
used as
template parameters for Equal
(in the example above,
std::shared_ptr<Base>
and std::shared_ptr<Derived>
).
D
need not derive from B
. Instead, (B, D)
must
satisfy one of the following requirements (such a pair is called
comparable):
D
is not abstract, inherits fromB
and implementsbool operator==(const D&) const
B
is ashared_ptr<T>
(orunique_ptr<T>
) andD
is ashared_ptr<U>
(orunique_ptr<U>
), and(T, U)
satisfies 1., 2. or 3.B
is atuple<Ts...>
andD
istuple<Us...>
of the same lengthn
so that for alli=0, ..., n-1
,(Ts[i], Us[i])
satisfies 1., 2. or 3.
(B, D)
-equality is then defined recursively: Let x
and
y
be instances of B
.
- In case 1.,
x
is(B, D)
-equal toy
ifx == y
. - In case 2.,
x
is(B, D)
-equal toy
if they can successfully be cast fromstd::shared_ptr<T>
tostd::shared_ptr<U>
, yielding elementsxder
andyder
, and ifxder
is(T, U)
-equal toyder
- In case 3.,
x = (x0, ..., xN)
is(B, D)
-equal toy = (y0, ..., yM)
ifN == M
and for eachi=0, ..., N
,x[i]
is(B[i], D[i])
equal toy[i]
If a parameter is not comparable (for example, a class from a
third-party library that you do not have any control over), this
parameter can forcibly made comparable using the DRMOCK_DUMMY
macro,
at least if bool operator==(const Foo&) const
is not deleted.
Regarding almost_equal
, the algorithm of comparison is the same as that
defined in the chapter basic.md. The precision of the
comparison may be set using
template<typename T> drmock::almost_equal(T expected, T abs_tol, T rel_tol)
or by #define
-ing DRTEST_*_TOL
, as described in
basic.md.
Beware of using the correct types! The following call is not allowed:
b.expects("foo", drmock::almost_equal(1.0)); // Using double, not float...
Failing to following this rule will lead to a potentially confusing error message like this:
/Users/malte/drmock/tests/Behavior.cpp:424:5: error: no matching member function for call to 'expects'
b.expects("foo", almost_equal(1.0), poly1);
~~^~~~~~~
/Users/malte/drmock/src/mock/Behavior.h:76:13: note: candidate function not viable: no known conversion from 'std::shared_ptr<ICompare<double>>' to 'detail::expect_t<float>' (aka 'variant<float, std::shared_ptr<ICompare<float>>>') for 2nd argument
Behavior& expects(detail::expect_t<Args>...);
^
/Users/malte/drmock/src/mock/Behavior.h:77:38: note: candidate function template not viable: no known conversion from 'std::shared_ptr<ICompare<double>>' to 'detail::expect_t<float>' (aka 'variant<float, std::shared_ptr<ICompare<float>>>') for 2nd argument
template<typename... Ts> Behavior& expects(detail::expect_t<Args>...);
^
/Users/malte/drmock/src/mock/Behavior.h:70:59: note: candidate function template not viable: requires 0 arguments, but 3 were provided
std::enable_if_t<(std::tuple_size_v<T> > 0), Behavior&> expects();
Mocking an operator declared in an interface is not much different from mocking any other method, only the way in which the mocked method is accessed from the mock object changes. Instead of
foo->mock.operator*().expects(/* ... */);
(which is illegal), you must use
foo->mock.operatorAst().expects(/* ... */);
The illegal tokens are replaced with a designator describing the operators symbol. The designators for C++'s overloadable operators are found in the table below.
Symbol | Designator | Symbol | Designator |
---|---|---|---|
+ |
Plus |
|= |
PipeAssign |
- |
Minus |
<< |
StreamLeft |
* |
Ast |
>> |
StreamRight |
/ |
Div |
>>= |
StreamRightAssign |
% |
Modulo |
<<= |
StreamLeftAssign |
^ |
Caret |
== |
Equal |
& |
Amp |
!= |
NotEqual |
| |
Pipe |
<= |
LesserOrEqual |
~ |
Tilde |
>= |
GreaterOrEqual |
! |
Not |
<=> |
SpaceShip |
= |
Assign |
&& |
And |
< |
Lesser |
|| |
Or |
> |
Greater |
++ |
Increment |
+= |
PlusAssign |
-- |
Decrement |
-= |
MinusAssign |
, |
Comma |
*= |
AstAssign |
->* |
PointerToMember |
/= |
DivAssig |
-> |
Arrow |
%= |
ModuloAssign |
() |
Call |
^= |
CaretAssign |
[] |
Brackets |
&= |
AmpAssign |
co_await |
CoAwait |
Thus, operator+
gets the handle operatorPlus
, operator%=
gets
operatorModuloAssign
, etc.
As of version 0.6
, the DrMock specification no longer requires
that the interface be abstract. All pure and non-pure virtual methods
are now (re-)implemented using the code generated by drmock-generator
.
The mock object implements a forwarding constructor which forwards any
parameters to the base class constructor.
mock/MockMacros.h
currently declares the DRMOCK_DUMMY
macro.
Using DRMOCK_DUMMY(Foo)
defines a trivial operator==
as follows:
inline bool operator==(const Foo&, const Foo&)
{
return true;
}
This can be used to make all third-party classes that occur as method parameters in an interface comparable.
However, you may not want this to end up in production code. Header
files created by drmock-generator
define the DRMOCK
macro for the
purpose of protecting other DRMOCK
macros from the preprocessor in
production code:
#ifdef DRMOCK
DRMOCK_DUMMY(QQuickWindow)
#endif
Beware! This macro must be used outside of any namespace,
and the parameter Foo
must specify the full namespace of the target class.
Also note that adding a semicolon ;
at the end of the macro call
may (depending on your compiler) result in an
extra ‘;’ [-Werror=pedantic]
error.
If Foo::operator==
is deleted
and you control the source code of Foo
,
you can ignore the delete in test/mock code by using the DRMOCK
macro:
class Foo
{
#ifndef DRMOCK
bool operator==(const Foo&) const = delete;
#endif
/* ... */
}
#ifdef DRMOCK
DRMOCK_DUMMY(Foo)
#endif
Recall that the CMake macro drmock_library
calls the Python script
drmock-generator
, which creates the source code of mock objects, and
compiles the source code into a C++ library.
drmock_library(
TARGET <target>
HEADERS header1 [header2 ...]
[IFILE <ifile>]
[MOCKFILE <mock_file>]
[ICLASS <iclass>]
[MOCKCLASS <mockclass>]
[LIBS lib1 [lib2 ...]]
[QTMODULES module1 [module2 ...]]
[INCLUDE include1 [include2 ...]]
[FRAMEWORKS framework1 [framework2 ...]]
[OPTIONS option1 [option2 ...]]
[FLAGS flag1 [flag2 ...]]
)
You may find detailed documentation in DrMockMacros.cmake.
The name of the output of drmock-generator
is determined by matching
each input filename (with extensions removed) against <ifile>
and
replacing the subexpression character \\1
in <mockfile>
with the
content of the unique capture group of <ifile>
, then adding on the
previously removed file extension. The class name of the mock object is
computed in analogous fashion.
The <ifile>
/<iclass>
regex must contain exactly one capture group, and
the <mockfile>
/<mockclass>
must contain exactly one backreference
character \1
.
The INCLUDE
, LIBS
, QTMODULES
and FRAMEWORK
parameters are used
to specify lists of additional includes, dynamic libraries to link
against, Qt modules to use and macOS framework paths to use.
If you run drmock-generator --help
, you will see that
drmock-generator
offers some customizability. drmock_library
exposes
these parameters through the OPTIONS
and FLAGS
parameters: The items
of the OPTIONS
list are forwarded to drmock-generator
directly. The
FLAGS
parameter is for passing compiler flags to drmock-generator
(note that clang
is the compiler that drmock-generator
uses).
items of the FLAGS
are forwarded to the --flags
keyword of the
drmock-generator
and used as target_compile_options
for TARGET
.
Only --access
, --namespace
, --controller
and
--clang-library-file
should be specified under OPTIONS
.
--input-class
and --output-class
are derived from the regular
expressions passed to drmock_library
. To specify --flags
, use the
FLAGS
parameter.
Throughout this tutorial, a couple of default behaviors and settings
have been mentioned, such as the name of the controller object
control
. Sometimes, you may wish to change these using the OPTIONS of
drmock-generator
. For details, use drmock-generator --help
.
-
By default,
drmock-generator
mocks all virtual methods, regardless of their access specifier. If you only wish to mock methods with a specific access specifier, use--access
. For example,--access public protected
mocks all non-private methods. -
By default, mock objects are placed in the same namespace as the interface. If you instead wish to place them in a different namespace, use
--namespace
. Use a leading::
to specify a global namespace; otherwise, the namespace you provide is considered to be relative to the namespace of the interface. For example, ifouter::inner::Interface
is the interface and you specify--namespace ::path::to::namespace
, then the mock object is placed in::path::to::namespace
. If you specify--namespace path::to::namespace
, the mock object is placed ininner::outer::path::to::namespace
. -
drmock_library
will try to detect the path to thelibclang
library automatically, either through CMake'sfind_library
or by checking the environment variableCLANG_LIBRARY_PATH
. Should this fail, you can specify the path manually using--clang-library-path
. -
If you wish to rename the
Controller
(this will be necessary if you have a method calledcontrol
), use--control NAME
to specify a new name for the member.
We mention some requirements for mocking an interface. For details, see DrMock specification.
When declaring a method in an interface, all type references occuring in the declaration of the parameters and return values must be declared with their full enclosing namespace.
In other words, this is wrong:
namespace drmock { namespace samples {
class Foo {};
class Bar {};
class IBaz
{
public:
virtual ~IBaz() = default;
Bar func(Foo) const = 0;
};
}} // namespace drmock::samples
Instead, the declaration of func
must be:
drmock::samples::Bar func(drmock::samples::Foo) const = 0;
When mocking Interface
, observe the following rules:
- The interface's name must not contain the substring
DRMOCK
- If the declaration contains an operator with symbol
SYMBOL
, thenInterface::
must not contain a method calledoperator{designator(SYMBOL)}
, wheredesignator
is defined according to the table in Operators Interface::
must not have a membermock
Interface::
must not have a member whose name contains the substringDRMOCK
- Every parameter type used in a virtual function must be comparable