I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 638 posts at DZone. You can read more from them at their website. View Full User Profile

Don't overspecify your mocks

10.15.2012
| 3458 views |
  • submit to reddit

Behavior-based verification is opposed to state-based verification in the fact that its assertion aren't performed on the values and objects returned by the System Under Test. Instead, they are made on the messages that the SUT sends - in the Java/PHP object-oriented paradigm, on the calls that are made on other objects.

We can specify lots of checks to perform on method calls, by substituting the real collaborators of the object or system under test with Mock objects. However, we usually are not keen on listing every single call to every single collaborator and method, as it makes for a verbose test. This kind of tests is also more difficult to maintain, as everytime the interface between the objects changes a bit, they all have to be updated.

So in the spirit of Don't Repeat Yourself, and do not write more code than is necessary, we can cut down on our mock expectations as much as possible. This article helps you do that with PHPUnit's syntax, but most of the concepts apply to all xUnit frameworks.

What is not a Mock expectation

Even if PHPUnit generates all kinds of test doubles via getMock(), and all libraries for test doubles are called mocking frameworks, not all of the configuration you can specify is part of a Mock in the original sense: behavior-based verification.

For example, return types and values are not of interest in a Mock; they would be in a Stub, but Mocks are focused on verifying the messages than the object under test sends to its collaborators, not the data that travel in the opposite sense.

Besides not specifying return values to simplify interfaces, you don't want to write down too strict expectations, specifying all the exact calls to all methods.

For example, today I was baffled by a test failing that contained this code:

$mock->expects($this->at(2))
     ->method('doSomething')
     ->with('argument')
     ->will($this->returnValue('result'));
$mock->expects($this->at(3))
     ->method('doSomething')
     ->with('anotherArgument')
     ->will($this->returnValue('anotherResult'));

which, for the non-initiated, means that the method doSomething() will perform these checks and return the specified values just on the 3rd and 4th calls (and null in other cases).

You can imagine that changing slightly the implementation code will almost always break this test. This is an (admittedly extreme) example of how blindly specifying a fixed list of method calls results in more maintenance: a case of making too many assumptions about the interface.

But it's possible to loosen up the specification and throw away most of these checks, in all the cases when they don't suit your purposes. The only mandatory part is the method() name, as epxectations cannot by design span multiple methods.

(Actually they can, but only with at() matchers, which are an overconstraint anyway with respect to method names: it's like substituting a numeric array for an associative one.)

Quantity

Quanties can be specified with:

  • $this->never() (0 calls)
  • $this->once() (1 call)
  • $this->exactly($times) ($times calls)

However, $this->any() allow unlimited calls, which are especially helpful when you specify callbacks for example. I always advise to use $this->any() instead of $this->once() for all query methods (reading the state of an object), as it doesn't matter if a read operation is performed multiple times. In this case you're really building a Stub, unless you're testing a cache or a queue.

Note however that if you don't call a method whose expectation is $this->any(), no error is issued. Make sure this is what you expect. :)

Arguments

$this->with() accepts as many arguments as the method call to expect. However, if you specify scalar variables, the check is strict:

$expectation->with(true, 42);

(with $expectation in these examples I mean the return value of $mock->expects(), which is not the mock anymore but an expectation object instead.)

But other constraints can be used, especially if we don't know the actual value that will be passed, or if it changes very often (a date, a random number...):

$expectation->with($this->instanceOf('Iterator')) 

You can also ignore some arguments while matching only the interesting ones:

$expectation->with(42, $this->anything());

Programmatic expectations

When there is no bundled matcher that suits what you want to check on an argument, you can always resort to some custom PHP code:

$self = $this;
$expectation->will($this->returnCallback(function($argument) use ($self) {
    $self->assertTrue(/* ... make your checks here */);
});

In PHP 5.4, you can use $this directly in place of $self.

Note that if you want to reuse this expectation, you can create a subclass of PHPUnit_Framework_Constraint:

class Number extends PHPUnit_Framework_Constraint
{
    public static function even()
    {
        return new self();
    }

    protected function matches($other)
    {
        return $other % 2 == 0;
    }

    public function toString()
    {
        return 'is even';
    }

    /*
     * The beginning of failure messages is "Failed asserting that" in most
     * cases. This method should return the second part of that sentence.
     */
    protected function failureDescription($other)
    {
        return "is even";
    }
}

and create it like this:

$expectation->with(Number::even());

Conclusions

Don't stick to the basic $mock->expects($this->once())->method('name')->with(42)->will($this->returnValue(100)) pattern. PHPUnit (and most other mocking tools) are more flexible than you think, and tests can be refined to cut to the point instead of checking a million useless details.

Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags: