41

I've run into a strange issue with PHPUnit mock objects. I have a method that should be called twice, so I'm using the "at" matcher. This works for the first time the method is called, but for some reason, the second time it's called, I get "Mocked method does not exist.". I've used the "at" matcher before and have never run into this.

My code looks something like:

class MyTest extends PHPUnit_Framework_TestCase
{
    ...

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

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

    ...
}

When I run the test, I get:

Expectation failed for method name is equal to <string:exists> when invoked at sequence index 1.
Mocked method does not exist.

If I remove the second matcher, I do not get the error.

Has anyone run into this before?

Thanks!

rr.
  • 6,484
  • 9
  • 40
  • 48

7 Answers7

47

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"));
    }

}
rr.
  • 6,484
  • 9
  • 40
  • 48
  • 6
    Yes, but I think it is a bug in PHPUnit. The documentation says: Returns a matcher that matches when the method it is evaluated for is invoked at the given $index. – gphilip May 11 '11 at 14:35
  • 10
    Agree, plus it would be much easier and more useful to spy method calls if the at() index would be incremented on a per-method basis. – Andrea Fiore May 31 '11 at 12:33
  • 1
    Looks like just about any mistaken usage of expects will cause the "Mocked method does not exist" message. Good to know. – Brad Koch Oct 02 '12 at 00:17
  • 1
    That makes mocking more than one method virtually unusable – tacone Feb 21 '14 at 13:58
  • 1
    today it still works like that, and now that I fixed my test with this post, I slam my hat onto the floor – John Smith Dec 10 '14 at 23:40
  • 1
    Could it be, that in 2015 this works now by call of method not by call of object? Looks like that in my tests. And oh boy did I waste hours on that! – Calamity Jane Dec 07 '15 at 16:21
  • Argument of method `$this->at()` has to start from `1` not from `0`. – DzeryCZ Jan 26 '18 at 11:00
17

FYI, Not sure if its related, but I encountered the same thing, but not with the $this->at() method, for me it was the $this->never() method.

This raised the error

$mock->expects($this->never())
    ->method('exists')
    ->with('arg');

This fixed the error

$mock->expects($this->never())
    ->method('exists');  

It did the same thing when using the $this->exactly(0) method.

Hope this help someone.

yvoyer
  • 7,476
  • 5
  • 33
  • 37
  • Thanks! This was really helpful, I ran into the same problem and I was stuck (until I read your comment). – Radu Gasler Sep 25 '13 at 09:48
  • Thanks, that fixed my problem too. But I really have no idea why it worked. Can you shed some light on the matter? – Silviu G Feb 20 '14 at 09:12
  • 3
    @SilviuG, since the method is never supposed to be called, the expectation about the with arguments should never happen. So since the configured expectation for the method did not happen but was configured to happen (because of the with), the error is trigger. Hope it makes sense. I agree that the message is kind of weird for this error. – yvoyer Feb 21 '14 at 12:11
5

Try changing $this->at(1) to $this->at(2)

Jon B
  • 51,025
  • 31
  • 133
  • 161
Alan
  • 51
  • 1
  • 1
  • Worked for me. I've called: once for ````foo```` and 3 times for ````bar```` methods after foo. Starting ````bar```` expectation from 1 fixed problem. – ryabenko-pro Dec 02 '14 at 14:04
4

This is an unfortunate wording of the error message by PHPUnit.

Double check the order of your calls, like @rr's answer mentions.

For me, as far as I know with my own code, I should be using at(0) and at(1) respectively, but it wasn't until I used at(2) and at(3) instead that it worked. (I'm using session mocking in CakePHP.)

The best way to check the order is to get 'into' the called method and check what's passed. You can do that like this:

$cakePost = $this->getMock('CakePost');
$cakePost->expects($this->once())
->method('post')
->with(
    // Add a line like this for each arg passed
    $this->callback(function($arg) {
        debug("Here's what was passed: $arg");
    })
);
Tyler Collier
  • 11,489
  • 9
  • 73
  • 80
  • Holy hell, that's annoying! Thanks for this, I was really struggling with a unit test due to exactly this problem. – GordonM Oct 04 '18 at 09:57
1

As far as i can tell from the Demo code it should work. I produced a working example in case you are running an older PHPUnit Version and want to check that way if it works for you too.

In case that doesn't help maybe you could provide a bit more (at best executable) code ? :)

<?php

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('exists')
             ->with($this->equalTo('bar'))
             ->will($this->returnValue(false));

        $this->assertTrue($mock->exists("foo"));
        $this->assertFalse($mock->exists("bar"));
    }

}

class MyClass {

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

printing

phpunit MyTest.php
PHPUnit 3.4.15 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 4.25Mb

OK (1 test, 3 assertions)
edorian
  • 38,542
  • 15
  • 125
  • 143
0

Are you sure you included MyClass in your test? I have had some undefined method errors when mocking a class/interface without including it.

martinvium
  • 556
  • 5
  • 4
0

May be not when question was raised however today documentation clearly specifies how the at should be used and I quote

Note
The $index parameter for the at() matcher refers to the index, starting at zero, in all method invocations for a given mock object. Exercise caution when using this matcher as it can lead to brittle tests which are too closely tied to specific implementation details.

Mubashar
  • 12,300
  • 11
  • 66
  • 95