Page 1 of 1
PHPUnit + Mock
Posted: Fri May 09, 2008 4:41 am
by jjrumi
Hi all,
I'm trying to test a controller and I want to isolate it from the models it uses.
For doing so, I want to mock my Factory class (the one that instantiates all the models) and some models.
The example below mocks only one model and tries to return it everytime the Factory is called to instantiate it.
Code: Select all
...
// Mocked model.
$mockUsersEditprofileModel = $this->getMock( 'UsersEditprofileModel', array( 'checkCurrentEmailPass' ) );
$mockUsersEditprofileModel->expects( $this->any() )
->method( 'checkCurrentEmailPass' )
->will( $this->returnValue( true ) );
// Mocked Factory object
// Here is where I get the problems.
$supFactory = $this->getMock( 'SuperFactory', array( 'getClass' ), array( $this->config ) );
$supFactory->expects( $this->any() )
->method( 'getClass' )->with( $this->equalTo('UsersEditprofileModel') )
->will( $this->returnValue( $mockUsersEditprofileModel ) );
$this->obj = new UsersCancelationController();
$this->obj->setInstance();
$this->obj->setFactory( $supFactory ); // Here I replace the current Factory for the mocked one.
...
When I execute this, I receive the following error:
Fail: "testRightObjectCreated" -> Expectation failed for method name is equal to when invoked zero or more times. Mocked method does not exist.
I think the problem comes when I use "->with( $this->equalTo('UsersEditprofileModel') )" in the $supFactory. There's no error when I skip the with part and I leave it with expects()->method()->will()... but of course it doesn't work for me because I want the Factory class to instantiate many models, not always the one I've mocked.
Any suggestions?

Re: PHPUnit + Mock
Posted: Fri May 09, 2008 7:05 am
by Weirdan
jjrumi wrote:
When I execute this, I receive the following error:
Fail: "testRightObjectCreated" -> Expectation failed for method name is equal to when invoked zero or more times. Mocked method does not exist.
I think the problem comes when I use "->with( $this->equalTo('UsersEditprofileModel') )" in the $supFactory. There's no error when I skip the with part and I leave it with expects()->method()->will()... but of course it doesn't work for me because I want the Factory class to instantiate many models, not always the one I've mocked.
Any suggestions?

'Mocked method does not exist' suggest that you're trying to express expectations on the method you didn't mock (for your Model, you mocked 'cancelAccount' but expectation are set on 'checkCurrentEmailPath'). It's weird, though, that you do not receive this error when omitting ->with() part in factory expectations
Re: PHPUnit + Mock
Posted: Fri May 09, 2008 11:58 am
by jjrumi
'Mocked method does not exist' suggest that you're trying to express expectations on the method you didn't mock (for your Model, you mocked 'cancelAccount' but expectation are set on 'checkCurrentEmailPath'). It's weird, though, that you do not receive this error when omitting ->with() part in factory expectations
Thanks for answering Weirdan.
Damn it! I copied it wrong!
The "mocking" of the Model is fine, I have no problems with it. It's with the Factory where I get the problems. (I've just edited the post).
So, as I said, is with the Factory that I have problems. Whenever I try to mix $mock->expects()->method()->with()->will()
I don't even know if what I'm trying to do is possible... but seems natural to me.
Re: PHPUnit + Mock
Posted: Fri May 09, 2008 3:47 pm
by Weirdan
Code: Select all
<?php
class TestFactory
{
public function getImplementation($className)
{
}
}
class TestTest extends PHPUnit_Framework_TestCase
{
public function testSameDistinguishTwoObjects()
{
$obj = new stdClass;
$obj2 = new stdClass;
$this->assertNotSame($obj, $obj2);
}
public function testWithIsSupportedWithWill()
{
$factory = $this->getMock('TestFactory');
$obj = new stdClass;
$factory->expects($this->any())
->method('getImplementation')
->with('TestObj')
->will($this->returnValue($obj));
$this->assertSame($obj, $factory->getImplementation('TestObj'));
}
public function testWithActsRegardlessOfTheCallSequence()
{
$factory = $this->getMock('TestFactory');
$obj = new stdClass;
$obj2 = new stdClass;
$factory->expects($this->any())
->method('getImplementation')
->with('obj1')
->will($this->returnValue($obj));
$factory->expects($this->any())
->method('getImplementation')
->with('obj2')
->will($this->returnValue($obj2));
$this->assertSame($obj, $factory->getImplementation('obj1'));
$this->assertSame($obj2, $factory->getImplementation('obj2'));
$this->assertSame($obj, $factory->getImplementation('obj1'));
}
}
?>
Here, only third case fails:
Code: Select all
TestTest
PHP Parse error: syntax error, unexpected T_STRING, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or '}' in /home/weirdan/
public_html/enm/build/q.php on line 4
PHPUnit 3.1.0beta4 by Sebastian Bergmann.
..F
Time: 0 seconds
There was 1 failure:
1) testWithActsRegardlessOfTheCallSequence(TestTest)
Failed asserting that two strings are equal.
expected string <obj2>
difference < x>
got string <obj1>
/home/weirdan/public_html/enm/build/q.php:43
FAILURES!
Tests: 3, Failures: 1.
As you can see, latter expectation overridden the former - thus it's not what you expected. It appears what you need is not supported by PHPUnit directly - the closest you can get without extending PHPUnit is onConsecutiveCalls:
Code: Select all
<?php
public function testOnConsecutiveCallsCanBeUsedToGenerateDifferentReturnValues()
{
$factory = $this->getMock('TestFactory');
$obj = new stdClass;
$obj2 = new stdClass;
$factory->expects($this->any())
->method('getImplementation')
->will($this->onConsecutiveCalls($obj, $obj2, $obj));
$this->assertSame($obj, $factory->getImplementation('obj1'));
$this->assertSame($obj2, $factory->getImplementation('obj2'));
$this->assertSame($obj, $factory->getImplementation('obj1'));
}
?>
Though PHPUnit is quite easy to extend with functionality you need:
Code: Select all
<?php
class PHPUnit_Framework_MockObject_Stub_ParameterBasedAction implements PHPUnit_Framework_MockObject_Stub
{
private $parameters = array();
private $returns = array();
private $currentParameters = null;
public function with()
{
if (!is_null($this->currentParameters)) {
throw new Exception('Out of sequence call - expecting ->will()');
}
$args = func_get_args();
$this->currentParameters = new PHPUnit_Framework_MockObject_Matcher_Parameters($args);
return $this;
}
public function will($stub)
{
if (is_null($this->currentParameters)) {
throw new Exception('Out of sequence call - expecting ->with()');
}
if (!($stub instanceof PHPUnit_Framework_MockObject_Stub)) {
$stub = new PHPUnit_Framework_MockObject_Stub_Return($stub);
}
$this->parameters[] = $this->currentParameters;
$this->returns[] = $stub;
$this->currentParameters = null;
return $this;
}
public function willBe($return)
{
return $this->will($return);
}
public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation)
{
foreach ($this->parameters as $i => $matcher) {
if ($matcher->matches($invocation)) {
return $this->returns[$i]->invoke($invocation);
}
}
throw new Exception(
'No matching parameters found for this invocation: '
. PHPUnit_Util_Type::toString($invocation)
);
}
public function toString()
{
return 'not implemented yet';
}
}
class TestFactory
{
public function getImplementation($className)
{
}
}
class TestTest extends PHPUnit_Framework_TestCase
{
public function testParameterBasedAction()
{
$factory = $this->getMock('TestFactory');
$obj = new stdClass;
$obj2 = new stdClass;
$factory->expects($this->any())
->method('getImplementation')
->will(
$this->whenCalled()
->with('obj1')->willBe($obj)
->with('obj2')->will($this->returnValue($obj2))
->with('qq')->will($this->throwException(new Exception('test exception')))
);
$this->assertSame($obj, $factory->getImplementation('obj1'));
$this->assertSame($obj2, $factory->getImplementation('obj2'));
$this->assertSame($obj, $factory->getImplementation('obj1'));
try {
$factory->getImplementation('qq');
$this->fail('Should have thrown exception');
} catch (Exception $e) {
// passes
}
}
public function whenCalled()
{
return new PHPUnit_Framework_MockObject_Stub_ParameterBasedAction;
}
}
?>
Re: PHPUnit + Mock
Posted: Tue May 13, 2008 4:59 am
by jjrumi
Thanks for the answer, dude. (And sorry for the delay, we had a long weekend in Spain

)
It has helped me to understand more the structure of PHPUnit.
With your solution is possible to mock the factory class method FOR THE SPECIFIED PARAMS but it doesn't work for calls like:
Code: Select all
$factory->getImplementation('obj23')
where obj23 is an object not declared as an expected parameter. The error I receive is this:
Fail: "testFormEntrance" -> Parameter 0 for invocation SuperFactory::getClass() does not match expected value.
The only solution I've found so far is make something like this:
Code: Select all
class SuperFactoryWithMocking extends SuperFactory
{
public static $mocking = array();
public function getClass( $classType )
{
if( !array_key_exists( $classType, SuperFactoryWithMocking::$mocking ) )
{
return parent::getClass( $classType );
}
else
{
return SuperFactoryWithMocking::$mocking[$classType];
}
}
}
and add in the array of the extended class all the models I want to mockin the test, although it's not my best option since I expected the Framework of PHPUnit to do it for me.
Re: PHPUnit + Mock
Posted: Tue May 13, 2008 8:24 am
by jjrumi
Uffff, PHPUnit has just thrown me a low punch...
After mocking a class like this:
Code: Select all
class RegistrationModel extends Model
{
const CANCEL_SEED_KEY = "pimpampum";
...
}
and trying to use the constant with
RegistrationModel::CANCEL_SEED_KEY I receive the following error:
Fatal error: Undefined class constant 'CANCEL_SEED_KEY' in ...
The mocking process doesn't take into account the constants defined in the class!!!
Doing this in the tested code:
Code: Select all
$test = new ReflectionClass( 'RegistrationModel ' );
var_dump( $test->getConstants() );
prompts that there are no constants in the class :_____(
Re: PHPUnit + Mock
Posted: Tue May 13, 2008 9:12 am
by Chris Corbyn
jjrumi wrote:The mocking process doesn't take into account the constants defined in the class!!!
Doing this in the tested code:
Code: Select all
$test = new ReflectionClass( 'RegistrationModel ' );
var_dump( $test->getConstants() );
prompts that there are no constants in the class :_____(
I don't believe it should. If it mocked class constants then your production code would need to be aware of the generated mock class. Where is the code which tries to access the constant and why is it trying to access a constant for a generated mock class name? That said, I'd have thought PHPUnit would subclass the original to produce a mock with the correct type.
Re: PHPUnit + Mock
Posted: Tue May 13, 2008 10:04 am
by jjrumi
Let's seeee... I may have misunderstood some concept of the whole testing process.
I have something like this (simplifying...):
Code: Select all
/**
* Extending SuperFactory class to return Mocked Models.
* SuperFactory is the class used to instantiate classes.
*/
class SuperFactoryWithMocking extends SuperFactory
{
// Array of classes I want to be mocked.
public static $mocking = array();
/**
* Override the getClass() function.
* Returns an instance of the class requested. The mocked object if it's in the array.
*/
public function getClass( $classType )
{
if( !array_key_exists( $classType, SuperFactoryWithMocking::$mocking ) )
{
return parent::getClass( $classType );
}
else
{
return SuperFactoryWithMocking::$mocking[$classType];
}
}
}
/**
* Unit Testing for UsersCancelationController
*/
class testUsersCancelationControllerTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
// Set up of the Mocked Model.
$mockRegistrationCancelationModel = $this->getMock( 'RegistrationModel', array( 'cancelUserRegistration' ) );
$mockRegistrationCancelationModel->expects( $this->any() )
->method( 'cancelUserRegistration' )
->will( $this->returnValue( true ) );
SuperFactoryWithMocking::$mocking['RegistrationCancelationModel'] = $mockRegistrationCancelationModel;
$this->obj = new UsersCancelationController();
// Substitution of the Factory class in the Controller so it uses the extended here.
$this->obj->setFactory( new SuperFactoryWithMocking( ) );
}
public function testLetsTestSomething()
{
$this->params = array();
$this->obj->setParams( $this->params );
$returnedValue = $this->obj->build();
$this->assertTrue( $returnedValue, 'Oooooh! It has returned false!' );
}
}
/**
* The tested class !!!
*/
class UsersCancelationController extends Controller
{
// This code is executed when the test is performed.
public function build()
{
...
// Instantiation of the Model mocked.
$user = $this->getClass( 'RegistrationModel' );
...
$user->cancelUserRegistration(); //Works fine, returns an straight TRUE as I have specified in the expected return value of the mocking object.
...
echo RegistrationModel::CANCEL_SEED_KEY; // This produces the Fatal Error "Undefined class constant 'CANCEL_SEED_KEY' in...".
...
return $whatever;
}
}
/**
* One of the models used by the tested Controller.
*/
class RegistrationModel extends Model
{
const CANCEL_SEED_KEY = "pimpampum";
...
public function cancelUserRegistration()
{
// Queries the Database...
}
}
Was this clearer, Chris?
Re: PHPUnit + Mock
Posted: Sat May 31, 2008 6:36 pm
by Weirdan
Chris Corbyn wrote:That said, I'd have thought PHPUnit would subclass the original to produce a mock with the correct type.
It does.
jjrumi wrote:
Was this clearer, Chris?
Not really. It doesn't even seem to do anything with mocks at all. According to the code you've posted you're accessing constant of a class (not mock) defined in the same file as your controller. PHPUnit shouldn't (can't) affect calls like that.
What does
Code: Select all
var_dump(in_array('RegistrationModel', get_declared_classes()));
print when placed where 'echo RegistrationModel::CANCEL_SEED_KEY' is now.
The mocking process doesn't take into account the constants defined in the class!!!
Doing this in the tested code:
Code: Select all
$test = new ReflectionClass( 'RegistrationModel ' ); // <------ here's a trailing space
var_dump( $test->getConstants() );
I noticed a space in the classname in the above fragment. Was that intentional?