使用at()功能的PHPUNIT模拟工作很奇怪

here is the following code sample

<?php

interface iFS
{
    public function read();
    public function write($data);
}

class MyClass
{
    protected $_fs = null;

    public function __construct(iFS $fs)
    {
        $this->_fs = $fs;
    }

    public function run(array $list)
    {
        foreach ($list as $elm)
        {
            $this->_fs->write($elm);
        }

        return $this->_fs->read();
    }
}

class MyTests extends PHPUnit_Framework_TestCase
{
    public function testFS()
    {
        $mock = $this->getMock('iFS');
        $mock->expects($this->at(0))
                ->method('read')
                ->will($this->returnValue('tototatatiti'));

        $c = new MyClass($mock);
        $result = $c->run(array('toto', 'tata', 'titi'));

        $this->assertEquals('tototatatiti', $result);
    }
}

This is absolutely not a real case but it make happen something strange with phpunit and at($index) feature.

My question is pretty simple, is it normal that the test fails?

I explicitely asked to return "tototatatiti", but it never happens...

When

  • I remove the line $this->_fs->write($elm); or
  • I replace $mock->expects($this->at(0)) by $mock->expects($this->once())

The test pass to green

Is there something I don't understand?

EDIT:

$mock->expects($this->at(3)) ->method('read') ->will($this->returnValue('tototatatiti'));

=> Will make the test pass green...

I think the phpunit at() features is not usefull to stub different return result for a mocked method if the mocked object contains some other methods witch are calls too...

If you want to test something like:

$stub->expects($this->at(0))
                ->method('read')
                ->will($this->returnValue("toto"));

$stub->expects($this->at(1))
                ->method('read')
                ->will($this->returnValue("tata"));

You should better use something like

$stub->expects($this->exactly(2))
                ->method('read')
                ->will($this->onConsecutiveCalls("toto", "tata));

According to PHPUnit source code we have:

public function matches(PHPUnit_Framework_MockObject_Invocation $invocation)
{
    $this->currentIndex++;

    return $this->currentIndex == $this->sequenceIndex;
}

each time the PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex attempts to match an invocation, the protected variable $currentIndex is incremented, therefore your call to write first causes it to become 0, and then it does not match read.

The second call, to read causes the value to become 1, so it does not match either.

Looks like it indeed applies to the whole object, which is useful if you need to make sure that a sequence of calls occurs in a particular order.

For instance, assuming the write method was only called once, you could have something like:

$mock->expects($this->at(0))
            ->method('write');

$mock->expects($this->at(1))
            ->method('read')
            ->will($this->returnValue('tototatatiti'));

This ensures that the write method is indeed called before the read method.