Mockery: returning values and throwing exceptions

Last week, I had to wrote a piece of code that contains retry logic. Naturally, I want to test it. That proved trickier than expected.

The application code looks like this:


class Sender { protected $connection; public function send() { $success = false; $i = 0; do { $i++; try { $success = $this->doSend($i); } catch (SenderException $e) { $success = false; } } while (!$success && $i < 3); return $success; } protected function doSend($data) { // Can throw SenderException $response = $this->connection->send($data); if ('OK' === $response) { return true; } return false; } }

I specifically want to test the retry logic, so I have to mock the ::doSend() method. Then I can simulate the different outcomes (returning true or false, or throwing a SenderException).

I use Mockery to do the real work. It is a great library. If you don't know it yet, please check it out. I will wait right here...

Now, since ::doSend() is a protected method, Mockery must be instructed to allow that.

So after the first try, I ended up with:

    public function testItWillRetrySending()
    {
        $sender = M::mock('Sender');
        $sender->shouldAllowMockingProtectedMethods()

        $sender->shouldReceive('doSend')
            ->andReturn(false, new Exception());
    }

To my surprise, this did not work. Instead of throwing the exception, Mockery returns it. So my next try was this:

    $sender->shouldReceive('doSend')
        ->andReturn(false)
        ->andThrow(new Exception());

Another surprise: with this code, Mockery will always throw the exception, and ignore the first return value (false). After some debugging, I found out that Mockery just overwrites the return values in this case.

Fortunately, there is another way to return multiple return values: the ::andReturnUsing() method. It gives full control over the return values.

So I ended up with this testing code:


$return_value_generator = function () { static $counter = 0; $counter++; switch ($counter) { case 1: return false; case 2: throw new SenderException(); case 3: return true; default: throw new Exception("Should never reach this."); } }; $sender = M::mock('Sender'); $sender->shouldAllowMockingProtectedMethods() ->shouldReceive('doSend') ->andReturnUsing($return_value_generator);

This works perfectly. It feels a bit like a hack though. So if you know a better way or have any other remarks, please let me know.