Specifying some behaviour with chainable mock objects is similar to describing it in words. You may say for example:
In C++ code you might express the same like this:
chainer.expects(once()) .with(new IsEqual<int>(321)) .after("other-ident") .will(new ReturnStub(123)) .id("ident");
You need not provide all of these methods, only what you actually need. If you don't intend
to return a special value: omit will()
and the default return value
will be used.
For void methods you may apply isVoid()
instead of
will()
to explicitly
express this fact.
When a method is invoked on a chainable mock object, the mock object searches
through its expectations from first to last to find one that matches the
invocation. An expectation matches an invocation if all of its
matching rules match the invocation. After the invocation, the matching expectation
might stop matching
further invocations. For example, an expects(once())
expectation only matches once and will be ignored on future invocations while an
expects(atLeastOnce())
expectation will always be matched against invocations.
The original java implementation contained an additional method called
method()
which passed the desired method name.
This is completely missing in my C++ implementation. The reason is
simple: C++ does not support reflection and therefor an application can't look into itself or
create methods at runtime or do some other weird things. So I had to apply a handful of
macro-magic and templates instead. This led to the
fact that you have to set up a different helper object for each method
whereas jMock would use the same object for all calls of a mock object.
A note about the internals: each chain link returns a temporary builder object with
its own set of methods to influence the mocked method and object. See directory
builder
for most of the files. The first
method expects()
returns
an object of type InvocationMockerBuilder
which provides method
with()
. And the last method returns a type
IdentityBuilder
which contains id()
.
Chainable mock objects provide stubbed and expected methods. An expected method must occur as specified, otherwise the object will fail when verified. A stubbed method without specification may be invoked unlimited times or not at all but the test still passes.
In general, you will stub when you want to get some return value from a method whereas you expect when you intend to check the input parameters to a method. When invoked with a specification both methods do internally the same but you should use the correct one to express your intention.
The parameter to the methods expects()
and
stubs()
must be some kind of invocation counter. The
current release covers the most common cases with a set of classes. However these classes
should not be used directly as they are not very readable. You should use one of the
wrapper functions instead:
InvokeOnceMatcher
= once()
: exactly one single invocationInvokeCountMatcher
= exactly()
: an exact invocation countInvokeAtLeastMatcher
= atLeast()
: a minimum invocation countInvokeAtLeastOnceMatcher
= atLeastOnce()
: invoke the method at least onceInvokeAtMostMatcher
= atMost()
: a maximum invocation countnever()
: never invoke this methodAfter setting up the behaviour and actually invoking the mock object the matcher specifications are processed one after the other in the order given in your source file. So if you wanted to return three different values 111, 222 and 333 you would write:
chainer.stubs(once()).will(new ReturnStub(111)); chainer.stubs(once()).will(new ReturnStub(222)); chainer.stubs(once()).will(new ReturnStub(333));
For obvious reasons you should not use atLeast()
in the first expectation
as is this would render the following lines useless.
Every single invocation to one of your mock objects may be labeled with a unique
identifier. This identifier may then be used to determine that another invocation
must happen prior or after it. You may as well demand that this decision has to be based
on an invocation to a totally different mock object. And it is also possible
to add more than one call to
before()
or after()
if you want to
have it match against more than one invocation in the same expectation chain.
When working with a VisitableMockObject
you normally specify exactly which parameter value you expect. A
ChainableMockObject
on the other hand can work less strict. Specifying an exact match is common but
you may as well allow a set of values or even any value.
Or you demand that each value must be higher than the one in the
previous invocation.
The distribution already contains a set of classes for common constraints. For convenience reasons and to enhance readability most of these classes have a counterpart in the form of a wrapper function. The most important ones are:
IsEqual
= eq()
tests equalityIsSame
= same()
tests if two objects are the sameIsCloseTo
= eq(a,b)
tests near equalityIsGreaterThan
= gt()
is true for higher values a reference valueIsLessThan
= lt()
is true for lower values than a reference valueIsNot
= logic_not()
negates another contraintAnd
= logic_and()
provides a logical-and of two constraintsOr
= logic_or()
provides a logical-or of two constraintsIsAnything
= any()
is always trueIsNothing
= nothing()
is never trueStringContains
= stringContains()
checks for the occurrence of a substringIsInstanceOf
= isA()
tests if an object is of a typeCreating your own constraints is quite easy. Look at the files in the
constraint
directory to get an idea how this can be done.
Caution with side effects | |
---|---|
One might try to to combine two contraints using mock.foo(logic_and<int>( eq<int>(0), outBound<int>(42))); Unfortunately the resulting behaviour might not be as expected. By default the evaluation of the two contstriaints is shortcut. This means that the second constraint is not evaluated when the result is determined after the first. This is intended as it is the default behaviour in C/C++: if(evalA() && evalB()) ...
In this case the second constraint is called one to little. If you need a more determined behaviour when all constraints are evaluated you can pass a third parameter which is set to false and enforces full evaluation. |
Use method with()
to pass the constraints. Obviously this
method has the same number of parameters as your mocked method.
There are special constraints which are not actually real constraints.
OutBound
does not check for incoming parameter values but
passes values back by a reference. This enables mocked methods like the following:
void getByReference(int &ra) { ra = 123; }
The values are returned one after the other. Please understand that for chainable mock objects this is probably not the desired behaviour as the values are already stored when the parameters are just evaluated. The intended application is for visitable mock objects.
Similarly there is TriggeredOutBound
which is used to store
the value defered upon a trigger call. The first call to eval()
does not store the value itself but a reference to the variable.
To store the actual value a stub class like TriggerStub
or
ReturnValueAndTriggerStub
is then
used to trigger the copy action. This way the storing of the desired outbound value
by reference is defered until the method would really be invoked. The calls to
eval()
are primarily to check for potential matches.
Unfortunately a temporary shared pointer variable is needed since all the objects are
completely independent from each other.
The following example would trigger the contraints in parameters 2 and 3 when the first parameters is 2:
class UserInfo { virtual void vident(unsigned userno, string &username, string &useradr) = 0; }; TriggeredOutbound<string> *tc_a; TriggeredOutbound<string> *tc_b; tc_a = new TriggeredOutbound<string>("my-name"); tc_b = new TriggeredOutbound<string>("my-address"); videnter.expects(once()) .with(eq(2u), tc_a, tc_b) .will(trigger(tc_a, tc_b));
mockpp contains two constraint classes to compare objects.
IsEqual
is probably appropriate for most cases. It takes
an object and stores it's value for later comparison.
IsSame
on the other hand is intended to check
for identical values. This usually means to compare the pointer values
of two objects. There is a template function
isSameComparison
which implements this default behaviour.
If you need a differing implementation for a certain class you can provide
a specialised template. This may happen if you are using a shared pointer and
don't want to compare the helper but the shared object at the end of
the pointers.
For a similar reason there is another templated function
named isEqualComparison
. Both functions are
useful if you need to compare objects but can't or don't want to
implement the according operator==
.
Potential backwards compatibility break | |
---|---|
mockpp up to version 1.9.3 implemented
The following fragment shows the problem. When constructing
the object a temporary string is created and this pointer is
passed to the constructor of IsSame<std::string> same ("string"); To avoid this problem you have to create a variable which must exist until the test completes. std::string str ("string"); IsSame<std::string> same (str);
|
The former sections explained standard methods for common use. But it is also possible to create
your own rules for matching an invocation. If your rules depend on the parameters passed
to the mocked method derive your custom class from InvocationMatcher
.
If it is independent from the parameters use TypelessMatcher
instead. To
express the non-standard use you should then pass the object to method
match()
.
But please keep in mind that custom matchers can virtually do everything. Most of the
time you want to check method parameters and therefor you probably should create a
constraint class derived from Constraint<T>
. See also the
files below mockpp/constraint
as a starting point.
Return values from methods and exceptions are treated similar. Exceptions are
considered not more than a special kind of return value. You may determine the return value
based on the result of the parameters you passed with expects()
and with()
or you can optionally set a default action
by passing it to setDefaultStub()
.
The default action takes place when no other expectation matches.
If you have a sequence of return values that shall be returned in subsequent
invocations you can use a short hand method and pass
the values via onConsecutiveCalls()
to
will()
.
To get a chainable mock object you must:
The following listing contains a short example and explains some of the features.
class MyInterfaceClass { public: virtual int access(unsigned index) = 0; }; class MyChainableMockObject : public ChainableMockObject, public MyInterfaceClass { public: MyChainableMockObject(const String &name) : ChainableMockObject(name, 0) , MOCKPP_CONSTRUCT_MEMBERS_FOR_CHAINABLE1(access) {} MOCKPP_CHAINABLE1(MyChainableMockObject, int, access, unsigned); }; MyChainableMockObject mco("mco"); MOCKPP_CHAINER_FOR(MyChainableMockObject, access) chainer (&mco); chainer.setDefaultStub(new ThrowStub<int>(make_throwable(1))); chainer.stubs() .with(eq(1)) .will(new ReturnStub<int>(13)); chainer.expects(once()) .with((eq(2))) .id("called-with-2"); chainer.stubs() .after(otherObject, "some-label") .will(new ReturnStub<int>(2222)); // use the object mco.verify();
Define your interface class. | |
Derive your mock object class from | |
According to the type of method and the number of parameters it takes add a macro that does the construction part for all the necessary internal helper variables for that method. | |
Similarly add another macro that implements the internal variables and helper methods. | |
Create a chainer object for the method to set up it's behaviour. | |
Add some behaviour: tell the object to throw an exception when no other expectations match. | |
More specific behaviour: depending on the parameters passed it shall respond with certain return values. | |
Add a label to an invocation for further reference. | |
After an invocation of some other object's method has occured return a different value. | |
After the tests have completed verify all conditions that are still pending, for example unused return values or exceptions. |
class MyChainableMockObject : public ChainableMockObject, public MyInterfaceClass { public: MyChainableMockObject(const String &name) : ChainableMockObject(name, 0) , access_mocker("access", this) {} virtual int access(unsigned index) { return access_mocker.forward(index); } ChainableMockMethod1<int, unsigned> access_mocker; }; MyChainableMockObject mco("mco"); ChainableMockMethod1<int, unsigned> &chainer (mco.access_mocker); chainer.setDefaultStub(new ThrowStub<int>(make_throwable(1))); chainer.stubs() .with(eq(12u)) .will(new ReturnStub<int>(13)); // use the object mco.verify();
Construct the mock method helper object. | |
Provide the method as entry point and forward the call to the method which does the actual work. | |
Instantiate a mock method variable. Select the class according to the parameters the method takes. There also a template without a trailing number which handles an arbitrary parameter count. | |
For convenience create a reference to the mock method helper. | |
Set up the mock objects behaviour in the same manner as described above using the macros. |