Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to assert mock object's function invocation's object arguments

Tags:

php

phpunit

Consider

class Foo {
    public $att;
    public function __construct ( $a ) { $this->att = $a; }
}

class Some {
    public function callMe ( Foo $f ) {}
}

// class I want to test
class SuT {
    public function testMe ( Some $s ) {
        echo $s->callMe( new Foo('hi') );
    }
}

I want to check whether Sut::testMe() properly invokes Some::callMe(). Since the parameter is a (Foo) object (not a scalar type), I can't figure out how to call PHPUnit's with() to run assertions on it. There is an assertAttributeEquals method for example, but how do I feed it the invocation's argument?

What I'd like to do is this:

class SuTTest extends PHPUnit_Framework_TestCase {
    public function testSuT () {
        $stub = $this->getMock( 'Some' );
        $stub->expects( $this->once() )->method( 'callMe' )
            ->with( $this->assertAttributeEquals('hi', 'att', $this->argument(0) );

        /*
         * Will $stub->callMe be called with a Foo object whose $att is 'hi'?
         */
        $sut = new SuT();
        $sut->testMe( $stub );
    }
}
like image 267
Jeroen Versteeg Avatar asked May 11 '11 10:05

Jeroen Versteeg


2 Answers

You just pass the expected values to the "with" method.

->with(1, $object, "paramThree");

you can also pass in a range of phpUnit assertions instead of the parameters (it defaults to equal to)

->with(1, $this->equalTo($object), "paramThree");

so for objects you would use $this->isInstanceOf("stdClass") as a parameter to ->with

For the list of possible assertions look into: PHPUnit/Framework/Assert.php

for functions that return a new PHPUnit_Framework_Constraint


Small Demo

The first testcase just matches 2 arguments and works

The second one matches two and fails on argument 2

The last one tests that the passed in object is of type stdClass

<?php

class MockMe {
    public function bla() {

    }
}

class Demo {

    public function foo(MockMe $x) {
        $x->bla(1, 2);
    }

    public function bar(MockMe $x) {
        $x->bla(1, new stdClass());
    }

}

class DemoTest extends PHPUnit_Framework_TestCase {

    public function testWorks() {
        $x = new Demo();
        $mock = $this->getMock("MockMe");
        $mock->expects($this->once())->method("bla")->with(1,2);
        $x->foo($mock);
    }

    public function testFails() {
        $x = new Demo();
        $mock = $this->getMock("MockMe");
        $mock->expects($this->once())->method("bla")->with(1,3);
        $x->foo($mock);
    }

    public function testObject() {
        $x = new Demo();
        $mock = $this->getMock("MockMe");
        $mock->expects($this->once())->method("bla")->with(1, $this->isInstanceOf("stdClass"));
        $x->bar($mock);
    }
}

Results in:

phpunit DemoTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.

.F.

Time: 0 seconds, Memory: 4.25Mb

There was 1 failure:

1) DemoTest::testFails
Failed asserting that <integer:2> matches expected <integer:3>.

...DemoTest.php:12
...DemoTest.php:34

FAILURES!
Tests: 3, Assertions: 2, Failures: 1.
like image 141
edorian Avatar answered Oct 19 '22 15:10

edorian


Even though the question is already answered fully (how to assert attribute of object passed as argument to mock) I think it's worth noting that PHPUnit supports a callback constraint to be passed to with().

I kept bumping with this thread while trying to find out how to run further assertions on the mocked object arguments. For instance, I needed to check the return value of some method. Obviously, and for sanity sake, there is no methodReturnValueEqualTo() equivalent to the attribute assertion used in the answer above.

Fortunately, PHPUnit does support (as of 3.7 at least) a callback constraint, which makes a lot of sense and is something you can find in specialised Mock libraries like Mockery.

Current PHPUnit version docs state the following:

The callback() constraint can be used for more complex argument verification. This constraint takes a PHP callback as its only argument. The PHP callback will receive the argument to be verified as its only argument and should return TRUE if the argument passes verification and FALSE otherwise.

Therefore, using the callback constraint, the OP example can now be expressed as:

class SuTTest extends PHPUnit_Framework_TestCase {
    public function testSuT () {
        $stub = $this->getMock('Some');
        $stub->expects($this->once())
            ->method('callMe')
            ->with($this->callback(function($arg) {
                    return ($arg instanceof Some) && ($arg->att === 'hi');
                })
            );

        /*
         * Will $stub->callMe be called with a Foo object whose $att is 'hi'?
         */
        $sut = new SuT();
        $sut->testMe($stub);
    }
}

And a test fail would look something like:

1) SuTTest::testSuT
Expectation failed for method name is equal to <string:callMe> when invoked 1 time(s)
Parameter 0 for invocation Some::callMe(Foo Object (...)) does not match expected value.
Failed asserting that Foo Object () is accepted by specified callback.

You can now run any logic on that argument.

Even better, despite not being a documented feature in PHPUnit docs, you can actually use assertions and get the benefits of assertion error messages:

class SuTTest extends PHPUnit_Framework_TestCase {
    public function testSuT () {
        // alias to circumvent php closure lexical variable restriction
        $test = $this;
        // proceed as normal
        $stub = $this->getMock('Some');
        $stub->expects($this->once())
            ->method('callMe')
            // inject the test case in the closure
            ->with($this->callback(function($arg) use ($test) {
                    // use test assertions
                    $test->assertInstanceOf('Some', $arg);
                    $test->assertAttributeEquals('hi', 'att', $arg);
                    // return true to satisfy constraint if all assertions passed
                    return true;
                })
            );

        /*
         * Will $stub->callMe be called with a Foo object whose $att is 'hi'?
         */
        $sut = new SuT();
        $sut->testMe( $stub );
    }
}

Don't really know how future proof is this strategy of using assertions and returning true. It is not documented and there is a tradeoff in the error message. You no longer get the parameter constraint message, so if you set assertions on multiple arguments you will have to infer, if possible, which one failed. But you do get a better description of the failed assertion inside the closure.

1) SuTTest::testSuT
Expectation failed for method name is equal to <string:callMe> when invoked 1 time(s)
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'hi'
+'no'

Hope this helps anyone facing a similar problem.

like image 31
Andre Torgal Avatar answered Oct 19 '22 14:10

Andre Torgal