It's a blog

Misled by PHPUnit at() method

So it turns out the at() method doesn’t quite do what I had initially thought….

I have recently been working on some tests for the new Newsletter extension for Mediawiki, specifically to test the NewslettterTablePager class. This thing extends the TablePager class in Mediawiki which is designed to make displaying information from a database table on a special page on a mediawiki site easy, and also easily enable things such as sorting.

The code interacts with the database and gets a ResultWrapper object, and the Pager uses the numRows(), seek() and fetchObject() methods, all of which I thought would be incredibly simple to mock.

Attempt 1

My first attempt where I first notice I have been thinking about the at() method all wrong can be seen below:

   private function getMockDatabase( array $resultObjects ) {
        $mockResult = $this->getMock( 'ResultWrapper' );
        $mockResult->expects( $this->atLeastOnce() )
            ->method( 'numRows' )
            ->will( $this->returnValue( count( $resultObjects ) ) );
        $mockResult->expects( $this->any() )
            ->method( 'seek' );
        foreach ( $resultObjects as $index => $resultObject ) {
            $mockResult->expects( $this->at( $index ) )
                ->method( 'fetchObject' )
                ->will( $this->returnValue( $resultObject ) );
        $mockDb = $this->getMock( 'IDatabase' );
        $mockDb->expects( $this->atLeastOnce() )
            ->method( 'select' )
            ->will( $this->returnValue( $mockResult ) );
        return $mockDb;

This methods returns a mock Database that the Pager will use. As you can see the only parameter is an array of objects to be returned by fetchObject() and I am using the at() method provided by phpunit to return each object at the index that it is stored in the array. This is when I discovered that at() in phpunit does not work in the way I first thought…

at() refers to the index of calls made to the mocked object as a whole. This means that in the code sample above, all of the calles to numRows() and seek() are increasing the current call counter index for the object and thus my mocked fetchObject() method is never returning the correct value or returning null.

Attempt 2

In my second attempt I made a guess that phpunit might allow multiple method mocks to stack and thus the return values of those methods be returned in the order that they were created. Thus I changed my loop to simply use any():

       foreach ( $resultObjects as $index => $resultObject ) {
            $mockResult->expects( $this->any() )
                ->method( 'fetchObject' )
                ->will( $this->returnValue( $resultObject ) );

But of course this also does not work and this result in the same $resultObject being returned for all calls.

Final version

I ended up having to to do something a little bit nasty (in my opinion) and use returnCallback() and use a private member of the testcase within the callback as a call counter / per method index:

       $testcase = $this;
        $mockResult->expects( $this->any() )
            ->method( 'fetchObject' )
            ->will( $this->returnCallback( function () use ( $testCase, $resultObjects ) {
                $obj = $resultObjects[$testCase->mockSeekCounter];
                $testCase->mockSeekCounter =+ 1;
                return $obj;
            } ) );


It would be great if phpunit would have some form of per method index expectation!

Some of the code examples here are slightly cut down, the full change can be seen at in the NewsletterTablePagerTest file.

If only I had Googled the issue sooner! (Blog post now dead :()


  1. Gijs

    Another option is to use a Generator (from PHP 5.5.0) (
    ->will($this->returnCallback(function () {
    return $this->getData()->current();
    And then have a function like:
    private function getData()
    yield “value 1”;
    yield “value 2”;

  2. Aryeh Gregor

    Is there a reason not to use onConsecutiveCalls()?

    • addshore

      onConsecutiveCalls sounds great!

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2021 Addshore

Theme by Anders NorénUp ↑

%d bloggers like this: