Monday, February 25, 2013

Piotr's Less Obvious Advice on Google Mock: State maintenance

Google Mock provides several ways to maintain state inside mock objects. One way of implementing state maintenance is with SaveArg. Consider the following example.

We have a class Configurator, which allows a caller to set and get values of a parameter:

class Configurator
{
    public:

    virtual ~Configurator() {}

    virtual void setParamX(int n) = 0;
    virtual int getParamX() = 0;
};


And we have a class Client that calls Configurator's methods and it also has a method incParamXBy, that can be used to increase the current value of paramX by a certain value.

class Client
{
    public:

    Client(Configurator & cfg);
    virtual ~Client() {}

    void setParamX(int n);
    void incParamXBy(int n);
    int getParamX();

    private:

    Configurator & _cfg;
};

incParamXBy internally calls setParamX and getParamX on Configurator:

void Client::incParamXBy(int n)
{
    _cfg.setParamX(_cfg.getParamX() + n);
}

Let's assume that the initial value of paramX is A and that we want to increase paramX by B each time we call incParamXBy. Our expectation is that if incParamXBy is called for the first time, it will result in calling cfg.setParamX(A+B).
  • second call of incParamXBy(B) will result in calling cfg.setPAramX(A + 2*B)
  • third call: cfg.setPAramX(A + 3*B), and so on...

Interaction between Client and Configurator in this case is such that Client relies on Configurator (issues getParamX()) in order to correctly calculate the parameter for next setParamX() call. If we want to test Client object, we need a MockConfigurator that is able to remember its current value of the parameter we are increasing:

class MockConfigurator : public Configurator
{
    public:

    int paramX;
    int * paramX_ptr;

    MockConfigurator()
    {
        paramX = 0;
        paramX_ptr = &paramX;
    }
   
    MOCK_METHOD1(setParamX, void(int n));
    MOCK_METHOD0(getParamX, int());
};

MockConfigurator has a public member paramX. We will use paramX to store current value of the parameter. We could have put paramX outside of MockConfigurator, in the test code, but I think it makes more sense to bind the parameter with the object.

And now the test:

MockConfigurator cfg;
Client client(cfg);

int inc_value = 10;

//getParamX will be called a number of times.
//If it is called, we will return the value pointed to by paramX_ptr.
//Returning with ReturnPointee is necessary, since we need to have
//the actual (updated) value each time the method is called.

EXPECT_CALL(cfg, getParamX())
    .Times(AnyNumber())
    .WillRepeatedly(ReturnPointee(cfg.paramX_ptr));

//SaveArg stores the 0th parameter of the call in the value pointed to by paramX_ptr (paramX)
//expectation 3
EXPECT_CALL(cfg, setParamX(cfg.paramX + 3*inc_value))
    .Times(1)
    .WillOnce(DoAll(SaveArg<0>(cfg.paramX_ptr), Return()));
//expectation 2
EXPECT_CALL(cfg, setParamX(cfg.paramX + 2*inc_value))
    .Times(1)
    .WillOnce(DoAll(SaveArg<0>(cfg.paramX_ptr), Return()));
//expectation 1
EXPECT_CALL(cfg, setParamX(cfg.paramX + inc_value))
    .Times(1)
    .WillOnce(DoAll(SaveArg<0>(cfg.paramX_ptr), Return()));

client.incParamXBy(inc_value); //this will match expectation 1
client.incParamXBy(inc_value);
//this will match expectation 2
client.incParamXBy(inc_value); //this will match expectation 3

Is it the only way to maintain state inside a mock object? Of course not. First of all, we could have an equivalent test without maintaining state inside out mock object at all. We could have written all method calls and all expectations using pre-calculated values that we know validate Client's behaviour. The point is that state maintenance gives us more flexibility that may be advantageous in case we want to write a number of similar tests (e.g. test the same behaviour for a range of parameter values or test different behaviours that rely accessing value previously stored in a mock).

Secondly, we can always implement state maintenance by using Invoke. Calling another function or method allows us not only to an equivalent to SaveArg, but also more complex state maintenance cases.

See also