1.3. Advanced Expectations

1.3.1. Introduction to Advanced Mock Objects

1.3.1.1. The Standard Working Method

To emulate the behaviour of an object in your production code you need more than just single values that meet your expectations. Usually you need a collection of the different expectation types and some code to generate a deterministic and predictable behaviour to test a limited area.

The following methods all require a separation of the code under test and the code using it. But most of the times you would split the two parts anyway to increase maintainability.


  class MyInterface 1
  {
    public:

      virtual void theMethod(unsigned param) = 0;
  };

  class MyProductionClass : public MyInterface 2
  {
    public:

      virtual void theMethod(unsigned param)
      {
        ...
      }
  };

  class MyMockObject : public MyInterface 3
  {
      ExpectationList<unsigned> methParams;

    public:

      MyMockObject(String name, Verifiable *v)
        : MockObject(name, v)
        , methParams("method parameters", this)
      {
        methParams.addExpected(1);
        methParams.addExpected(2);
      }

      virtual void theMethod(unsigned param)
      {
        methParams.addActual(param);
        ...
      }
  };


  MyMockObject mo (MOCKPP_PCHAR("mockObject"), 0);
  myMethodUnderTest(&mo); 4
  mo.verify();


  MyProductionClass pro;
  myMethodUnderTest(&pro); 5

1

Define an interface class which contains all the methods that describe some real world behaviour. This interface class is also used to implement the real world class.

The intention is to pass a real world object in the running application or a mock object while executing tests.

2

Derive your real world production code from the abstract base class to implement our interface as you need it.

3

Derive the mock object and implement it with a minimum of the functionality required in an absolutely deterministic way. In the example we set up a list of expected parameters to the method and have it verified by the mock object automatically.

4

Instantiate the mock object and pass it to the method you want to test while you are in the test routines. After completing the test code call verify() to check all pending expectations.

5

To run the production code instantiate the real object and pass it to the method.

1.3.1.2. The Main Mock Object Classes

Since most of the tasks concerning mock objects occur frequently and in a similar manner there are some classes which support automated behaviour with different approaches. Depending on your personal taste or special problems you may choose the one or the other.

  • The first possibility is modeled by a class called VisitableMockObject. It was named like this because you set up part of its behaviour by recording an execution path for the tests, thus by "visiting" the object. This approach is rather strict because the invocation order and parameter values must match exactly. Unfortunately it is almost impossible to add new capabilities.
  • The second approach is represented by the class ChainableMockObject. The name stems from the fact that you define the behaviour with a "chain" of invocations of a helper object. Each chain link sets a constraint, a return value or some other aspect. The big benefit from this approach is the extensibility concerning the behaviour by simply feeding it new objects which reflect your needs. Therefor it is far more flexible in execution: everything depends on user defined objects outside the mock object. A large set of such classes is already included.

More detailed explanations follow in later chapters.

1.3.1.3. How To Set Up Behaviour

The basic principle for the use of these classes is the same for both methods:

  • Derive from the interface class and the according mock object class.
  • Add the methods and their potential internal variables.
  • Use one of the available methods to construct a helper object to set up the detailed behaviour.

There are two standard ways to set up the behaviour of advanced mock objects.
  • The traditional way using macros which implement everything behind the scenes and an additional helper object which controls these helper objects.

    But caution: if you take the wrong macro or maybe just have a small typo you will get loads of error messages all pointing to the same line. I strongly recommend to take little steps when setting up such objects: add one single method and compile. To get started I recommend to use the "recipes" below.

    If you want to have an overview what is created behind the scenes, look at chainable-template.h and visitable-template.h which contain example classes with some methods. They were preprocessed, automatically indented and are quite readable. This knowledge might become useful when you need to find out more about the referenced objects and their internal helper variables when runtime errors occur.

  • The newer and recommended approach with the help of mock methods. The methods are templates which wrap the method under test. Since they are fully accessible in contrast to an obscure macro they are easier to use and better to debug.

    For both the Visitable and the Chainable types there are sets of templates for mock methods according to the number of parameters the method under test takes. But most of the current compilers support a shorthand version which selects the needed template automatically.

    So the following lines are equivalent:

    
      VisitableMockMethod0<void>           mmv0(MOCKPP_PCHAR("mm0"), 0);
      VisitableMockMethod1<void, int>      mmv1(MOCKPP_PCHAR("mm1"), 0);
      VisitableMockMethod2<void, int, int> mmv2(MOCKPP_PCHAR("mm2"), 0);
    
      VisitableMockMethod<void>            mmvd0(MOCKPP_PCHAR("mm0"), 0);
      VisitableMockMethod<void, int>       mmvd1(MOCKPP_PCHAR("mm1"), 0);
      VisitableMockMethod<void, int, int>  mmvd2(MOCKPP_PCHAR("mm2"), 0);
     

There is another point to keep in mind when choosing either the macros or the templates. There are only macros available for up to five parameters. Extending this number probably results in a lot of coding and debugging. The templates on the other hand are generated by several scripts. The standard distribution supports up to 6 parameters. If you need more you only have to invoke gen_files_N.sh in the top level directory of the source distribution. Supply the maximal number of parameters you need and all the header files are generated.

1.3.1.4. Choosing the macros

There are sets of macros for const and non-const methods, the visitable and the chainable way and each combination of them with up to five parameters. I assume you will not want to have code with more than 5 parameters. But if you really think you need more, take the macros with 5 parameters and try to extend it. It is really straightforward and please don't forget to extend the test files. You may then of course send me the result for inclusion and further maintainance.

Due to the nature of macros there are some limitations concerning the automatically generated methods and the according internal helper variables.

  • Every type in the parameter list may need a default constructor.
  • Pointer values may be compared with their address and not with the value they point to.
  • With the above macros only const & X types are possible in the parameter list. You don't actually see it but that's what internally happens. If you have a const parameter or a simple type you must use an extended set of macros which distinguishes between the method parameters and the declaration of the internal helper variables.
  • If you have overloaded methods you must use the extended macros as well. They extend the internal variables names which are derived form the method name. Otherwise there would be a conflict.
  • It may become necessary to implement operator<<() as debug output is created this way. One approach is to provide a meaningful operator method. If this does not make much sense you may use a default template instead which simply outputs the raw classname. To avoid pontential conflicts this template must be enabled by setting the macro MOCKPP_ENABLE_DEFAULT_FORMATTER before including the first mockpp header file.

    It is also possible to re-use an existing operator<<(std::ostream &, const T &val). In this case you supply depending on the string type one of the macros MOCKPP_OWSTREAMABLE or MOCKPP_OSTREAMABLE. This implements a simple string translator function.

The total number of macros is quite huge but there is a scheme behind it:

  • The macro for the initialiser list in the constructor is built like this:

    • Always start with MOCKPP_CONSTRUCT_MEMBERS_FOR_
    • If the method is of type void add VOID_
    • For chainable object append CHAINABLE
    • Visitable object need VISITABLE instead
    • Finally append the number of method parameters 0 .. 5

  • To determine the macro for the method write the following:

    • Always start with MOCKPP_
    • If the method is of type void add VOID_
    • In case the method is of type const write CONST_
    • For chainable object append CHAINABLE
    • whereas visitable object need VISITABLE
    • Extended macros with explicit method types need _EXT
    • Finally append the number of method parameters 0 .. 5

  • The macro for the helper object to set up the behaviour needs these steps:

    • Always start with MOCKPP_
    • For chainable object append CHAINER_FOR
    • whereas visitable object need CONTROLLER_FOR
    • Extended macros with explicit method types need _EXT

Here are some examples:

  • A void method with 5 parameters in a chainable mock object: MOCKPP_VOID_CHAINABLE5
  • A void and const method without parameters in a visitable mock object and extended parameter values and method name: MOCKPP_VOID_CONST_VISITABLE_EXT0
  • Macro for initialiser list for a void method with 2 parameters: MOCKPP_CONSTRUCT_MEMBERS_FOR_VOID_CHAINABLE2

To see examples for all the macros have a look at the unit test files in the tests directory. Especially interesting are the files ChainableMockObject_* and VisitableMocObject_*.

If the available mock objects and methods don't give you fine enough control over your mock object there is only one solution: code and implement the desired behaviour manually. Look at chainable-template.h and visitable-template.h to get an idea of the standard implementation.

But I assume this will not happen too often because mock objects should only cover a very limited aspect, only what is currently really needed for a special test scope.

1.3.1.5. Automated Verification

After the last invocation to a mock object has occurred you should always invoke verify() on the mock object to verify all pending expectations. Maybe there are some unused return objects or outstanding calls. If you use CppUnit as test framework you should use VerifyingTestCase from mockpp and register your mock objects on creation with it. In contrast to the standard TestCase from the CppUnit package it invokes verify() on all mock objects after a test case passed. This way it is impossible to forget it.

Unfortunately this class is partially based on an error in reasoning and became more complicated as originally thought. So it is less useful as expected. Decide yourself if it is of any advantage in your application.