John Smith
John Smith

Reputation: 6207

Phpunit, mock, willReturnCallback() doesnt work as expected

class:

class TestMe
{
    public function m1 (array &$a)
    {
    }

    public function m2 (array &$a)
    {
    }

    public function methodd()
    {
        $a = array();
        $this->m1 ($a);
        $this->m2 ($a);
        return $a;
    }
}

test:

class X extends PHPUnit_Framework_TestCase
{
    public function testMethod()
    {
        $mock = $this->getMock('TestMe', array('m1','m2'));
        $mock->expects($this->once())->method('m1')->with(array())->willReturnCallback(function (&$x) { $x['a'] = 1; });
        $mock->expects($this->once())->method('m2')->with(array('a' => 1))->willReturnCallback(function (&$x) { $x['b'] = 2; });

        $x = $mock->methodd();

        $this->assertEquals (array('a' => 1, 'b' => 2), $x);
    }
}

somehow it fails:

There was 1 failure:

1) X::testMethod Expectation failed for method name is equal to when invoked 1 time(s). Parameter 0 for invocation TestMe::m1(Array (...)) does not match expected value. Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( + 'a' => 1 + 'b' => 2 )

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

I dont know what it can be. In other words, I want to modify a "reference" parameter, and check it :)

Upvotes: 0

Views: 11619

Answers (1)

Schleis
Schleis

Reputation: 43760

The problem isn't directly with willReturnCallback(). Your problem is that you are passing things around by reference. If you look at the your failure message, it is telling you that the parameters that you passed in to the method m1 don't match what was expected. It is saying that the array isn't empty.

PHPUnit checks the parameter calls on the mocks after the test was run. So it stores a copy of the parameters used in the method call. In your callback, you are referring to a reference of the variable. So PHPUnit stores its own copy of the reference in the mock to check. Upon subsequent calls to the function not only does the variable get updated but the value that PHPUnit stored. So when it checks that the mocks were called with the proper parameters, it checks the against the value stored in the reference which is the final value and not the value that was initially used.

To fix this, change your test to:

    public function testMethod()
{
    //Get a copy of the testcase to use in the callback
    $testcase = $this;
    $mock = $this->getMock('TestMe', ['m1','m2']);
    $mock->expects($this->once())
         ->method('m1')
         ->willReturnCallback(function (&$x) use ($testcase) {
             //do your parameter checking immediately
             $testcase->assertEquals([], $x); 
             $x['a'] = 1; 
         });
    $mock->expects($this->once())
         ->method('m2')
         ->willReturnCallback(function (&$x) use ($testcase) {
             //do your parameter checking immediately
             $testcase->assertEquals(['a' => 1], $x); 
             $x['b'] = 2; 
         });

    $x = $mock->method();

    $this->assertEquals (['a' => 1, 'b' => 2], $x);
}

Though IMO, you shouldn't be mocking code that is part of the class that you are testing. The fact that methodd() uses those functions is an implementation detail of your class. If you decided to move the logic found in m1() or m2 into methodd(), your tests should pass. Having these methods mocked makes your tests less useful. They can fail due to code changes in the class rather than actual changes in functionality/bugs.

Anyway, the fix to your specific problem would be to check the values of the parameters in your callback rather than using with().

Upvotes: 1

Related Questions