<?php | |
namespace Codeception\Module; | |
/** | |
* Unit testing module | |
* | |
* This is the heart of the CodeGuy testing framework. | |
* By providing a unique set of features Unit, the module makes your tests cleaner, more readable, and easier to write. | |
* | |
* ## Features | |
* * Descriptive - simply write what you are testing and how you are testing. | |
* * Execution limit - only 'execute* methods actually execute your code. It's easy to see where tested methods are invoked. | |
* * Simple stub definition - create stubbed class with one call. All properties and methods can be passed as callable functions. | |
* * Dynamic mocking - stubs can be automatically turned into mocks. | |
* | |
*/ | |
class Unit extends \Codeception\Module | |
{ | |
protected $stubs = array(); | |
protected $predictedExceptions = array(); | |
protected $thrownExceptions = array(); | |
protected $last_result; | |
/** | |
* @var \Codeception\TestCase | |
*/ | |
protected $test; | |
protected $testedClass; | |
protected $testedMethod; | |
protected $testedStatic; | |
public function _initialize() | |
{ | |
} | |
public function _before(\Codeception\TestCase $test) | |
{ | |
$this->test = $test; | |
$this->stubs = array(); | |
} | |
public function _after(\Codeception\TestCase $test) | |
{ | |
\Codeception\Util\Fixtures::cleanup(); | |
} | |
public function _failed(\Codeception\TestCase $test, $fail) | |
{ | |
if (count($this->stubs)) { | |
$this->debug("Stubs were used:"); | |
foreach ($this->stubs as $stub) { | |
if (isset($stub->__mocked)) $this->debug($stub->__mocked); | |
$this->debug(json_encode($stub)); | |
} | |
} | |
} | |
/** | |
* Registers a class/method which will be tested. | |
* When you run 'execute' this method will be invoked. | |
* Please, note that it also updates the feature section of the scenario. | |
* | |
* For non-static methods: | |
* | |
* ``` php | |
* <?php | |
* $I->testMethod('ClassName.MethodName'); // I will need ClassName instance for this | |
* ``` | |
* | |
* For static methods: | |
* | |
* ``` php | |
* <?php | |
* $I->testMethod('ClassName::MethodName'); | |
* ``` | |
* | |
* @param $signature | |
*/ | |
public function testMethod($signature) | |
{ | |
if (strpos($signature, '.')) { | |
// this is class method | |
list($class, $method) = explode('.', $signature); | |
$this->testedClass = $class; | |
$this->testedMethod = $method; | |
$this->testedStatic = false; | |
} elseif (strpos($signature, '::')) { | |
// we test static method | |
list($class, $method) = explode('::', $signature); | |
$this->testedClass = $class; | |
$this->testedMethod = $method; | |
$this->testedStatic = true; | |
} | |
$this->debug('Class: ' . $class); | |
$this->debug('Method: ' . $method); | |
if ($this->testedStatic) $this->debug('Static'); | |
} | |
/** | |
* Adds a stub to the internal registry. | |
* Use this command if you need to convert this stub to a mock. | |
* Without adding the stub to registry you can't trace it's method invocations. | |
* | |
* @param $instance | |
*/ | |
public function haveFakeClass($instance) | |
{ | |
$this->stubs[] = $instance; | |
$stubid = count($this->stubs) - 1; | |
if (isset($instance->__mocked)) $this->debugSection('Registered stub', 'Stub_' . $stubid . ' {' . $instance->__mocked . '}'); | |
} | |
/** | |
* Alias for haveFakeClass | |
* | |
* @alias haveFakeClass | |
* @param $instance | |
*/ | |
public function haveStub($instance) | |
{ | |
$this->haveFakeClass($instance); | |
} | |
/** | |
* Execute The tested method on an object (a stub can be passed). | |
* First argument is an object, the rest are supposed to be parameters passed to method. | |
* | |
* Example: | |
* | |
* ``` php | |
* <?php | |
* $I->wantTo('authenticate user'); | |
* $I->testMethod('User.authenticate'); | |
* $user = new User(); | |
* $I->executeTestedMethodOn($user, 'Davert','qwerty'); | |
* // This line $user->authenticate('Davert','qwerty') was called. | |
* $I->seeResultEquals(true); | |
* ?> | |
* ``` | |
* | |
* For static methods use 'executeTestedMethodWith'. | |
* | |
* @param $object | |
*/ | |
public function executeTestedMethodOn($object) | |
{ | |
$args = func_get_args(); | |
$obj = array_shift($args); | |
$method = $this->testedMethod; | |
$callable = function () use ($method, $obj, $args) { | |
$reflectedObj = new \ReflectionClass($obj); | |
$reflectedMethod = $reflectedObj->getMethod($method); | |
if (!$reflectedMethod) | |
throw new \Codeception\Exception\Module(__CLASS__,sprintf('Method %s can\'t be called in this object', $method)); | |
if (!$reflectedMethod->isPublic()) { | |
$reflectedMethod->setAccessible(true); | |
} | |
return $reflectedMethod->invokeArgs($obj, $args); | |
}; | |
$this->execute($callable); | |
if (isset($obj->__mocked)) $this->debug('Received Stub'); | |
$this->debug("Method {$this->testedMethod} executed"); | |
$this->debug('With parameters: ' . json_encode($args)); | |
} | |
/** | |
* Executes the tested static method with parameters provided. | |
* | |
* ``` | |
* <?php | |
* $I->testMethod('User::validateName'); | |
* $I->executeTestedMethodWith('davert',true); | |
* // This line User::validate('davert', true); was called | |
* ?> | |
* ``` | |
* For a non-static method use 'executeTestedMethodOn' | |
* | |
* @param $params | |
* @throws \Codeception\Exception\Module | |
*/ | |
public function executeTestedMethodWith($params) | |
{ | |
$args = func_get_args(); | |
if (!method_exists($this->testedClass, $this->testedMethod)) | |
throw new \Codeception\Exception\Module(__CLASS__,sprintf('%s::%s is not valid callable', $this->testedClass, $this->testedMethod)); | |
$class = $this->testedClass; | |
$method = $this->testedMethod; | |
$callable = function () use ($args, $class, $method) { | |
return call_user_func_array(array($class, $method), $args); | |
}; | |
$this->execute($callable); | |
$this->debug("Static method {$this->testedClass}::{$this->testedMethod} executed"); | |
$this->debug('With parameters: ' . json_encode($args)); | |
} | |
/** | |
* Executes the method which is being tested. | |
* If the method is not static, the class instance should be provided. | |
* | |
* If a method is static 'executeTestedWith' will be called. | |
* If a method is not static 'executeTestedOn' will be called. | |
* See those methods for the full reference | |
* | |
* @throws \InvalidArgumentException | |
*/ | |
public function executeTestedMethod() | |
{ | |
$args = func_get_args(); | |
if ($this->testedStatic) { | |
call_user_func_array(array($this, 'executeTestedMethodWith'), $args); | |
} else { | |
call_user_func_array(array($this, 'executeTestedMethodOn'), $args); | |
} | |
} | |
/** | |
* Executes a code block. The result of execution will be stored. | |
* Parameter should be a valid Closure. The returned value can be checked with seeResult actions. | |
* | |
* Example: | |
* | |
* ``` php | |
* <?php | |
* $user = new User(); | |
* $I->execute(function() use ($user) { | |
* $user->setName('Davert'); | |
* return $user->getName(); | |
* }); | |
* $I->seeResultEquals('Davert'); | |
* ?> | |
* ``` | |
* | |
* You can use native PHPUnit asserts in the executed code. | |
* These can be either static methods of the 'PHPUnit_Framework_assert' class, | |
* or functions taken from 'PHPUnit/Framework/Assert/Functions.php'. They start with 'assert_' prefix. | |
* You should manually include this file, as these functions may conflict with functions in your code. | |
* | |
* Example: | |
* | |
* ``` php | |
* <?php | |
* require_once 'PHPUnit/Framework/Assert/Functions.php'; | |
* | |
* $user = new User(); | |
* $I->execute(function() use ($user) { | |
* $user->setName('Davert'); | |
* assertEquals('Davert', $user->getName()); | |
* }); | |
* ``` | |
* | |
* @param \Closure $code | |
*/ | |
public function execute(\Closure $code) | |
{ | |
// cleanup mocks | |
foreach ($this->stubs as $mock) { | |
$mock->__phpunit_cleanup(); | |
} | |
$this->createMocks(); | |
$this->predictExceptions(); | |
$res = null; | |
try { | |
$res = $code->__invoke(); | |
} catch (\Exception $e) { | |
$this->catchException($e); | |
} | |
$this->debug('Result: ' . json_encode($res)); | |
$this->last_result = $res; | |
} | |
/** | |
* Executes a method of an object. | |
* Additional parameters can be provided. | |
* | |
* Example: | |
* | |
* ``` php | |
* <?php | |
* // to execute $user->getName() | |
* $I->executeMethod($user,'getName'); | |
* | |
* // to execute $user->setName('davert'); | |
* $I->executeMethod($user,'setName', 'davert'); | |
* | |
* // or more parameters | |
* $I->executeMethod($user, 'setNameAndAge', 'davert', '30'); | |
* | |
* ?> | |
* ``` | |
* | |
* @param $object | |
* @param $method | |
*/ | |
public function executeMethod($object, $method) { | |
$params = func_get_args(); | |
$object = array_shift($params); | |
$method = array_shift($params); | |
// cleanup mocks | |
foreach ($this->stubs as $mock) { | |
$mock->__phpunit_cleanup(); | |
} | |
$this->createMocks(); | |
$this->predictExceptions(); | |
$res = null; | |
try { | |
$res = call_user_func_array(array($object, $method), $params); | |
} catch (\Exception $e) { | |
$this->catchException($e); | |
} | |
$this->debug('Result: ' . json_encode($res)); | |
$this->last_result = $res; | |
} | |
/** | |
* Updates multiple properties of the selected object. | |
* Can update even private and protected properties. | |
* | |
* Properties to be updated and their values are passed in the second parameter as an array: | |
* array('theProperty' => 'some value', | |
* ('anotherProperty' => 'another value') | |
* | |
* @param $obj | |
* @param array $values | |
*/ | |
public function changeProperties($obj, $values = array()) { | |
//As for now, PHP cannot resolve properties of namespaced class if class instance passed to reflector | |
//In our case we have mocked class with original full classname stored in __mocked property | |
if(isset($obj->__mocked)) { | |
$reflectedObj = new \ReflectionClass($obj->__mocked); | |
} else { | |
$reflectedObj = new \ReflectionClass($obj); | |
} | |
foreach ($values as $key => $val) { | |
$property = $reflectedObj->getProperty($key); | |
$property->setAccessible(true); | |
$property->setValue($obj, $val); | |
} | |
} | |
/** | |
* Updates a single property of the selected object | |
* Can update even private and protected properties. | |
* | |
* @param $obj | |
* @param $property | |
* @param $value | |
*/ | |
public function changeProperty($obj, $property, $value) { | |
$this->changeProperties($obj, array($property => $value)); | |
} | |
public function seeExceptionThrown($classname, $message = null) { | |
\PHPUnit_Framework_Assert::assertNotEmpty($this->thrownExceptions, "No exception was thrown"); | |
foreach ($this->thrownExceptions as $e) { | |
if ($e instanceof $classname) { | |
\PHPUnit_Framework_Assert::assertInstanceOf($classname, $e); | |
if ($message) \PHPUnit_Framework_Assert::assertContains($message, $e->getMessage()); | |
return; | |
} | |
} | |
} | |
protected function predictExceptions() | |
{ | |
$this->thrownExceptions = array(); | |
$this->predictedExceptions = array(); | |
$scenario = $this->test->getScenario(); | |
$steps = $scenario->getSteps(); | |
for ($i = $scenario->getCurrentStep() + 1; $i < count($steps); $i++) { | |
$step = $steps[$i]; | |
$action = $step->getAction(); | |
if ($action == 'executeTestedMethod') break; | |
if ($action == 'executeTestedMethodOn') break; | |
if ($action == 'executeTestedMethodWith') break; | |
if ($action == 'execute') break; | |
if ($action == 'executeMethod') break; | |
if ($action != 'seeExceptionThrown') continue; | |
$args = $step->getArguments(false); | |
$this->predictedExceptions[] = $args[0]; | |
} | |
} | |
protected function catchException($e) | |
{ | |
$this->thrownExceptions[] = $e; | |
foreach ($this->predictedExceptions as $predicted) { | |
if ($e instanceof $predicted) return; | |
} | |
throw $e; | |
} | |
/** | |
* Very magical function that generates Mock methods for expected assertions | |
* Allows the declaration of seeMethodInvoked, seeMethodNotInvoked, etc, AFTER the 'execute' command | |
* | |
*/ | |
protected function createMocks() | |
{ | |
$scenario = $this->test->getScenario(); | |
$steps = $scenario->getSteps(); | |
if (!isset($steps[$scenario->getCurrentStep()])) throw new \Exception("New steps were added to scenario in realtime. Can't proceed.\nRemove loops from your unit test to fix it"); | |
for ($i = $scenario->getCurrentStep()+1; $i < count($steps); $i++) { | |
$step = $steps[$i]; | |
if (strpos($action = $step->getAction(), 'seeMethod') === 0) { | |
$arguments = $step->getArguments(false); | |
$mock = array_shift($arguments); | |
$function = array_shift($arguments); | |
$params = array_shift($arguments); | |
foreach ($this->stubs as $stub) { | |
if (get_class($stub) == get_class($mock)) { | |
$mock = $stub; | |
} | |
} | |
$invoke = false; | |
switch ($action) { | |
case 'seeMethodInvoked': | |
case 'seeMethodInvokedAtLeastOnce': | |
if (!$mock) throw new \InvalidArgumentException("Stub class not defined"); | |
$invoke = new \PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce(); | |
break; | |
case 'seeMethodInvokedOnce': | |
if (!$mock) throw new \InvalidArgumentException("Stub class not defined"); | |
$invoke = new \PHPUnit_Framework_MockObject_Matcher_InvokedCount(1); | |
break; | |
case 'seeMethodNotInvoked': | |
if (!$mock) throw new \InvalidArgumentException("Stub class not defined"); | |
$invoke = new \PHPUnit_Framework_MockObject_Matcher_InvokedCount(0); | |
break; | |
case 'seeMethodInvokedMultipleTimes': | |
if (!$mock) throw new \InvalidArgumentException("Stub class not defined"); | |
$times = $params; | |
if (!is_int($times)) throw new \InvalidArgumentException("Invoked times count should be an integer"); | |
$params = $arguments; | |
$invoke = new \PHPUnit_Framework_MockObject_Matcher_InvokedCount($times); | |
break; | |
default: | |
} | |
if ($invoke) { | |
$mockMethod = $mock->expects($invoke)->method($function); | |
$this->debug(get_class($invoke) . ' attached'); | |
if ($params) { | |
call_user_func_array(array($mockMethod, 'with'), $params); | |
$this->debug('with ' . json_encode($params)); | |
} | |
} | |
} | |
if ($step->getAction() == 'executeTestedMethod') break; | |
if ($step->getAction() == 'execute') break; | |
if ($step->getAction() == 'executeTestedMethodOn') break; | |
if ($step->getAction() == 'executeTestedMethodWith') break; | |
} | |
} | |
/** | |
* Checks that a method of a stub was invoked after the last execution. | |
* Requires a stub as the first parameter, the method name as the second. | |
* Optionally pass the arguments which are expected by the executed method. | |
* | |
* Example: | |
* | |
* ``` php | |
* <?php | |
* $I->testMethod('UserService.create'); | |
* $I->haveStub($user = Stub::make('Model\User'));* | |
* $service = new UserService($user); | |
* $I->executeTestedMethodOn($service); | |
* // we expect $user->save was invoked. | |
* $I->seeMethodInvoked($user, 'save'); | |
* ?> | |
* ``` | |
* | |
* This method dynamically creates a mock from a stub. | |
* | |
* @magic | |
* @see createMocks | |
* @param $mock | |
* @param $method | |
* @param array $params | |
*/ | |
public function seeMethodInvoked($mock, $method, array $params = array()) | |
{ | |
$this->verifyMock($mock); | |
} | |
/** | |
* Checks that a method of a stub was invoked *only once* after the last execution. | |
* Requires a stub as the first parameter, a method name as the second. | |
* Optionally pass the arguments which are expected by the executed method. | |
* | |
* Look for 'seeMethodInvoked' to see the example. | |
* @magic | |
* @see createMocks | |
* @param $mock | |
* @param $method | |
* @param array $params | |
*/ | |
public function seeMethodInvokedOnce($mock, $method, array $params = array()) | |
{ | |
$this->verifyMock($mock); | |
} | |
/** | |
* Checks that a method of a stub *was not invoked* after the last execution. | |
* Requires a stub as the first parameter, a method name as the second. | |
* Optionally pass the arguments which are expected by the executed method. | |
* @magic | |
* @see createMocks | |
* @param $mock | |
* @param $method | |
* @param array $params | |
*/ | |
public function seeMethodNotInvoked($mock, $method, array $params = array()) | |
{ | |
$this->verifyMock($mock); | |
} | |
/** | |
* Checks that a method of a stub was invoked *multiple times* after the last execution. | |
* Requires a stub as the first parameter, a method name as the second and the expected number of executions. | |
* Optionally pass the arguments which are expected by the executed method. | |
* | |
* Look for 'seeMethodInvoked' to see the example. | |
* @magic | |
* @see createMocks | |
* @param $mock | |
* @param $method | |
* @param $times | |
* @param array $params | |
*/ | |
public function seeMethodInvokedMultipleTimes($mock, $method, $times, array $params = array()) | |
{ | |
$this->verifyMock($mock); | |
} | |
protected function verifyMock($mock) | |
{ | |
if ($mock instanceof \Codeception\Maybe) $mock = $mock->__value(); | |
foreach ($this->stubs as $stubid => $stub) { | |
if (spl_object_hash($stub) == spl_object_hash($mock)) { | |
if (!$mock->__phpunit_hasMatchers()) { | |
continue; | |
// throw new \Exception("Probably Internal Error. There is no matchers for current mock"); | |
} | |
if (isset($stub->__mocked)) { | |
$this->debugSection('Triggered Stub', 'Stub_' . $stubid . ' {' . $stub->__mocked . '}'); | |
} | |
\PHPUnit_Framework_Assert::assertTrue(true); // hook to increment assertions counter | |
try { | |
$mock->__phpunit_verify(); | |
} catch (\PHPUnit_Framework_ExpectationFailedException $e) { | |
\PHPUnit_Framework_Assert::fail("\n" . $e->getMessage()); // hook to increment assertions counter | |
throw $e; | |
} | |
return; | |
} | |
} | |
throw new \Exception("Mock is not registered by 'haveStub' or 'haveFakeClass' methods"); | |
} | |
/** | |
* Asserts that the last result from the tested method is equal to value | |
* | |
* @param $value | |
*/ | |
public function seeResultEquals($value) | |
{ | |
$this->assert(array('Equals', $value, $this->last_result,'in '.$this->last_result)); | |
} | |
public function seeResultContains($value) | |
{ | |
\PHPUnit_Framework_Assert::assertContains($value, $this->last_result); | |
} | |
/** | |
* Checks that the result of the last execution doesn't contain a value. | |
* | |
* @param $value | |
*/ | |
public function dontSeeResultContains($value) | |
{ | |
\PHPUnit_Framework_Assert::assertNotContains($value, $this->last_result); | |
} | |
/** | |
* Checks that the result of the last execution is not equal to a value. | |
* | |
* @param $value | |
*/ | |
public function dontSeeResultEquals($value) | |
{ | |
\PHPUnit_Framework_Assert::assertNotEquals($value, $this->last_result); | |
} | |
/** | |
* Checks that the result of the last execution is empty. | |
*/ | |
public function seeEmptyResult() | |
{ | |
\PHPUnit_Framework_Assert::assertEmpty($this->last_result); | |
} | |
/** | |
* Checks that the result of the last execution is a specific type. | |
* Either 'int', 'bool', 'string', 'array', 'float', 'null', 'resource', 'scalar' can be passed for simple types. | |
* Otherwise the parameter must be a class and the result must be an instance of that class. | |
* | |
* Example: | |
* | |
* ``` php | |
* <?php | |
* $I->execute(function() { return new User }); | |
* $I->seeResultIs('User'); | |
* ?> | |
* ``` | |
* | |
* @param $type | |
*/ | |
public function seeResultIs($type) | |
{ | |
if (in_array($type, array('int', 'bool', 'string', 'array', 'float', 'null', 'resource', 'scalar'))) { | |
\PHPUnit_Framework_Assert::assertInternalType($type, $this->last_result); | |
return; | |
} | |
\PHPUnit_Framework_Assert::assertInstanceOf($type, $this->last_result); | |
} | |
/** | |
* Checks that the property of an object equals the value provided. | |
* Can check even protected or private properties. | |
* | |
* Bear in mind that testing non-public properties is not a good practice. | |
* Use it only if you have no other way to test it. | |
* | |
* @param $object | |
* @param $property | |
* @param $value | |
*/ | |
public function seePropertyEquals($object, $property, $value) | |
{ | |
$current = $this->retrieveProperty($object, $property); | |
$this->debug('Property value is: ' . $current); | |
\PHPUnit_Framework_Assert::assertEquals($value, $current); | |
} | |
/** | |
* Checks that the property is a passed type. | |
* Either 'int', 'bool', 'string', 'array', 'float', 'null', 'resource', 'scalar' can be passed for simple types. | |
* Otherwise the parameter must be a class and the property must be an instance of that class. | |
* | |
* Bear in mind that testing non-public properties is not a good practice. | |
* Use it only if you have no other way to test it. | |
* | |
* @param $object | |
* @param $property | |
* @param $type | |
*/ | |
public function seePropertyIs($object, $property, $type) { | |
$current = $this->retrieveProperty($object, $property); | |
if (in_array($type, array('int', 'bool', 'string', 'array', 'float', 'null', 'resource', 'scalar'))) { | |
\PHPUnit_Framework_Assert::assertInternalType($type, $current); | |
return; | |
} | |
\PHPUnit_Framework_Assert::assertInstanceOf($type, $current); | |
} | |
/** | |
* Executes a method and checks that the result is equal to a value. | |
* Good for testing values taken from getters. | |
* | |
* Example: | |
* | |
* ``` php | |
* $I->testMethod('User.setName'); | |
* $user = new User(); | |
* $I->executeTestedMethodOn($user, 'davert'); | |
* $I->seeMethodReturns($user,'getName','davert'); | |
* | |
* ``` | |
* * | |
* @param $object | |
* @param $method | |
* @param $value | |
* @param array $params | |
*/ | |
public function seeMethodReturns($object, $method, $value, $params = array()) | |
{ | |
$result = call_user_func_array(array($object, $method), $params); | |
\PHPUnit_Framework_Assert::assertEquals($value, $result); | |
} | |
/** | |
* Executes a method and checks that the result is NOT equal to a value. | |
* Good for testing values taken from getters. | |
* | |
* Look for 'seeMethodReturns' for example. | |
* | |
* @param $object | |
* @param $method | |
* @param $value | |
* @param array $params | |
*/ | |
public function seeMethodNotReturns($object, $method, $value, $params = array()) | |
{ | |
$result = call_user_func_array(array($object, $method), $params); | |
\PHPUnit_Framework_Assert::assertNotEquals($value, $result); | |
} | |
protected function retrieveProperty($object, $property) | |
{ | |
if (isset($object->__mocked)) $this->debug('Received STUB'); | |
$reflectionClass = new \ReflectionClass($object); | |
$reflectionProperty = $reflectionClass->getProperty($property); | |
$reflectionProperty->setAccessible(true); | |
return $reflectionProperty->getValue($object); | |
} | |