Mock使った時のMocked method does not exist.

PHPUnitでハマりにハマった内容残しときます。

テスト対象こんな感じ。

    public function modifyBlogEntriesProducts($articleId, $productIds) {
        $columnDao = $this->_getDao('blog/column');
        $products = $columnDao->selectBlogEntriesProductsList(array('blog_entry_id' => $articleId));
        if(!$products) {
            $this->entryBlogEntriesProductsByArray($articleId, $productIds);
        } else {
            $products = $products[0];
            //deleteする配列とinsertする配列を作る。
            $oldProductIds = $this->_getValuesArrayByKeyName($products, 'product_id');

            //insertList = 入力値にあってDBにない商品ID(=新たに追加する商品ID)
            $insertList = array_diff($productIds, $oldProductIds);
            //deleteList = DBにあって入力値にない商品ID(=削除しなければならない商品ID)
            $deleteList = array_diff($oldProductIds, $productIds);
            $this->deleteBlogEntriesProductsByArray($articleId, $deleteList);
            $this->entryBlogEntriesProductsByArray($articleId, $insertList);
        }
    }

誤ったテストコードこんな感じ。

    function testModifyBlogEntriesProducts2() {
        $methods = array('selectBlogEntriesProductsList'
                        , 'deleteBlogEntriesProducts'
                        , 'insertBlogEntriesProducts');
        $columnDao = $this->getMock('Dao_Blog_Sample', $methods);
        $returnObj = array(
                        array(
                            (object)array('product_id' => '789')
                            , (object)array('product_id' => '753')
                         )
                     );
        $columnDao->expects($this->once())
                    ->method('selectBlogEntriesProductsList')
                    ->with($this->equalTo(array('blog_entry_id' => '123')))
                    ->will($this->returnValue($returnObj));
        $columnDao->expects($this->once())
                    ->method('deleteBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('753'));
        $columnDao->expects($this->exactly(2))
                    ->method('insertBlogEntriesProducts');
        $columnDao->expects($this->at(1))
                    ->method('insertBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('456'));
        $columnDao->expects($this->at(2))
                    ->method('insertBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('012'));

        $mocks = array('blog/column' => $columnDao);
        $daoManager = new MockDaoManager();
        $daoManager->setMockInstance($mocks);

        $columnService = $this->_getService('blog/column', $daoManager);
        $columnService->modifyBlogEntriesProducts('123', array('456', '789', '012'));
    }

テスト結果こんな感じ。

phpunit service/Blog/SampleServiceTest.php 
PHPUnit 3.3.17 by Sebastian Bergmann.

....F

Time: 0 seconds

There was 1 failure:

1) testModifyBlogEntriesProducts2(SampleServiceTest)
Expectation failed for method name is equal to <string:insertBlogEntriesProducts> when invoked at sequence index 0.
Mocked method does not exist.
..../PHPUnit/Framework/MockObject/Mock.php(228) : eval()'d code:24

FAILURES!
Tests: 5, Assertions: 5, Failures: 1.

モックが作れない?とかなんとかってエラーメッセージだったのでなんで?とずーーーーっと悩んでた。
google先生に聞いて↓の回答みて気付いた。。
http://stackoverflow.com/questions/3367513/phpunit-mocked-method-does-not-exist-when-using-mock-expectsthis-at

The issue ended up being with how I understood the "at" matcher to work. Also, my example was not verbatim how it is in my unit test. I thought the "at" matcher counter worked on a per query basis, where it really works on a per object instance basis.

Example:

class MyClass {

    public function exists($foo) {
        return false;
    }

    public function find($foo) {
        return $foo;
    }
}
Incorrect:

class MyTest extends PHPUnit_Framework_TestCase
{

    public function testThis()
    {
        $mock = $this->getMock('MyClass');
        $mock->expects($this->at(0))
             ->method('exists')
             ->with($this->equalTo('foo'))
             ->will($this->returnValue(true));

        $mock->expects($this->at(0))
             ->method('find')
             ->with($this->equalTo('foo'))
             ->will($this->returnValue('foo'));

        $mock->expects($this->at(1))
             ->method('exists')
             ->with($this->equalTo('bar'))
             ->will($this->returnValue(false));

        $this->assertTrue($mock->exists("foo"));
        $this->assertEquals('foo', $mock->find('foo'));
        $this->assertFalse($mock->exists("bar"));
    }

}
Correct:

class MyTest extends PHPUnit_Framework_TestCase
{

    public function testThis()
    {
        $mock = $this->getMock('MyClass');
        $mock->expects($this->at(0))
             ->method('exists')
             ->with($this->equalTo('foo'))
             ->will($this->returnValue(true));

        $mock->expects($this->at(1))
             ->method('find')
             ->with($this->equalTo('foo'))
             ->will($this->returnValue('foo'));

        $mock->expects($this->at(2))
             ->method('exists')
             ->with($this->equalTo('bar'))
             ->will($this->returnValue(false));

        $this->assertTrue($mock->exists("foo"));
        $this->assertEquals('foo', $mock->find('foo'));
        $this->assertFalse($mock->exists("bar"));
    }

}

何が違うって
$mock->expects($this->at(1))
の数値が違う。
何?と思ってリファレンス↓
http://www.phpunit.de/manual/3.3/ja/test-doubles.html#test-doubles.mock-objects.tables.matchers
のatのトコ見てみて「あれ?」と。
評価対象全体で何回目に実行されたか、ということなので正解は

    function testModifyBlogEntriesProducts2() {
        $methods = array('selectBlogEntriesProductsList'
                        , 'deleteBlogEntriesProducts'
                        , 'insertBlogEntriesProducts');
        $columnDao = $this->getMock('Dao_Blog_Sample', $methods);
        $returnObj = array(
                        array(
                            (object)array('product_id' => '789')
                            , (object)array('product_id' => '753')
                         )
                     );
        $columnDao->expects($this->once())
                    ->method('selectBlogEntriesProductsList')
                    ->with($this->equalTo(array('blog_entry_id' => '123')))
                    ->will($this->returnValue($returnObj));
        $columnDao->expects($this->once())
                    ->method('deleteBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('753'));
        $columnDao->expects($this->exactly(2))
                    ->method('insertBlogEntriesProducts');
        $columnDao->expects($this->at(2))   //← この前にselectとdeleteがはしってるので評価対象全体では2回目。(0から数えるので。)
                    ->method('insertBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('456'));
        $columnDao->expects($this->at(3))   //← この前にselectとdeleteとinsertがはしってるので評価対象全体では3回目。(0から数えるので。)
                    ->method('insertBlogEntriesProducts')
                    ->with($this->equalTo('123'), $this->equalTo('012'));

        $mocks = array('blog/column' => $columnDao);
        $daoManager = new MockDaoManager();
        $daoManager->setMockInstance($mocks);

        $columnService = $this->_getService('blog/column', $daoManager);
        $columnService->modifyBlogEntriesProducts('123', array('456', '789', '012'));
    }

になります。

たしかにモックの実行順番からしたら1番目はdeleteBlogEntriesProductsだからinsertBlogEntriesProductsのモックは入れられないよーってメッセージは合ってる。
でもここらへんって日本語の詳細な使い方とか全然なくてリファレンスの内容結構スルーしてた…
ちゃんと読まなきゃだめだやっぱ。