diff --git a/src/Codeception/Specify.php b/src/Codeception/Specify.php index 53dc163..8e4af76 100644 --- a/src/Codeception/Specify.php +++ b/src/Codeception/Specify.php @@ -3,8 +3,10 @@ use Codeception\Specify\Config; use Codeception\Specify\ConfigBuilder; +use Codeception\Specify\ObjectProperty; -trait Specify { +trait Specify +{ private $beforeSpecify = array(); private $afterSpecify = array(); @@ -27,7 +29,7 @@ private function specifyInit() if (!$this->specifyConfig) $this->specifyConfig = Config::create(); } - function specify($specification, \Closure $callable = null, $params = []) + function specify($specification, \Closure $callable = null, $params = []) { if (!$callable) return; $this->specifyInit(); @@ -38,7 +40,7 @@ function specify($specification, \Closure $callable = null, $params = []) $this->setName($newName); - $properties = get_object_vars($this); + $properties = $this->getSpecifyObjectProperties(); // prepare for execution $throws = $this->getSpecifyExpectedException($params); @@ -58,13 +60,12 @@ function specify($specification, \Closure $callable = null, $params = []) if ($closure instanceof \Closure) $closure->__invoke(); } } + $this->specifyExecute($test, $throws, $example); // restore object properties - foreach ($properties as $property => $val) { - if ($this->specifyConfig->propertyIgnored($property)) continue; - $this->$property = $val; - } + $this->specifyRestoreProperties($properties); + if (!empty($this->afterSpecify) && is_array($this->afterSpecify)) { foreach ($this->afterSpecify as $closure) { if ($closure instanceof \Closure) $closure->__invoke(); @@ -120,8 +121,10 @@ private function specifyExecute($test, $throws = false, $examples = array()) } $result = $this->getTestResultObject(); + try { call_user_func_array($test, $examples); + $this->specifyCheckMockObjects(); } catch (\PHPUnit_Framework_AssertionFailedError $e) { if ($throws !== get_class($e)){ $result->addFailure(clone($this), $e, $result->time()); @@ -179,29 +182,86 @@ function cleanSpecify() } /** - * @param $properties - * @return array + * @param ObjectProperty[] $properties */ private function specifyCloneProperties($properties) { - foreach ($properties as $property => $val) { - if ($this->specifyConfig->propertyIgnored($property)) { - continue; - } - if ($this->specifyConfig->classIgnored($val)) { + foreach ($properties as $property) { + $propertyName = $property->getName(); + $propertyValue = $property->getValue(); + + if ($this->specifyConfig->classIgnored($propertyValue)) { continue; } - if ($this->specifyConfig->propertyIsShallowCloned($property)) { - if (is_object($val)) { - $this->$property = clone $val; + if ($this->specifyConfig->propertyIsShallowCloned($propertyName)) { + if (is_object($propertyValue)) { + $property->setValue(clone $propertyValue); } else { - $this->$property = $val; + $property->setValue($propertyValue); } } - if ($this->specifyConfig->propertyIsDeeplyCloned($property)) { - $this->$property = $this->copier->copy($val); + + if ($this->specifyConfig->propertyIsDeeplyCloned($propertyName)) { + $property->setValue($this->copier->copy($propertyValue)); } } } + + /** + * @param ObjectProperty[] $properties + */ + private function specifyRestoreProperties($properties) + { + foreach ($properties as $property) { + $property->restoreValue(); + } + } + + /** + * @return ObjectProperty[] + */ + private function getSpecifyObjectProperties() + { + $properties = []; + + foreach (get_object_vars($this) as $property => $value) { + if ($this->specifyConfig->propertyIgnored($property)) { + continue; + } + + $properties[] = new ObjectProperty($this, $property, $value); + } + + // isolate mockObjects property from PHPUnit_Framework_TestCase + if (($phpUnitReflection = $this->specifyGetPhpUnitReflection()) !== null) { + $properties[] = $mockObjects = new ObjectProperty( + $this, $phpUnitReflection->getProperty('mockObjects') + ); + + // remove all mock objects inherited from parent scope(s) + $mockObjects->setValue([]); + } + + return $properties; + } + + private function specifyCheckMockObjects() + { + if (($phpUnitReflection = $this->specifyGetPhpUnitReflection()) !== null) { + $verifyMockObjects = $phpUnitReflection->getMethod('verifyMockObjects'); + $verifyMockObjects->setAccessible(true); + $verifyMockObjects->invoke($this); + } + } + + /** + * @return \ReflectionClass|null + */ + private function specifyGetPhpUnitReflection() + { + if ($this instanceof \PHPUnit_Framework_TestCase) { + return new \ReflectionClass('\PHPUnit_Framework_TestCase'); + } + } } diff --git a/src/Codeception/Specify/ObjectProperty.php b/src/Codeception/Specify/ObjectProperty.php new file mode 100644 index 0000000..dcdc462 --- /dev/null +++ b/src/Codeception/Specify/ObjectProperty.php @@ -0,0 +1,78 @@ + + */ +class ObjectProperty +{ + /** + * @var mixed + */ + private $_owner; + + /** + * @var \ReflectionProperty|string + */ + private $_property; + + /** + * @var mixed + */ + private $_initValue; + + /** + * ObjectProperty constructor. + * + * @param $owner + * @param $property + * @param $value + */ + public function __construct($owner, $property, $value = null) + { + $this->_owner = $owner; + $this->_property = $property; + + if (!($this->_property instanceof \ReflectionProperty)) { + $this->_property = new \ReflectionProperty($owner, $this->_property); + } + + $this->_property->setAccessible(true); + + $this->_initValue = ($value === null ? $this->getValue() : $value); + } + + /** + * @return string + */ + public function getName() + { + return $this->_property->getName(); + } + + /** + * Restores initial value + */ + public function restoreValue() + { + $this->setValue($this->_initValue); + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->_property->getValue($this->_owner); + } + + /** + * @param mixed $value + */ + public function setValue($value) + { + $this->_property->setValue($this->_owner, $value); + } +} diff --git a/tests/ObjectPropertyTest.php b/tests/ObjectPropertyTest.php new file mode 100644 index 0000000..45c90dc --- /dev/null +++ b/tests/ObjectPropertyTest.php @@ -0,0 +1,66 @@ +prop = 'test'; + + $prop = new \Codeception\Specify\ObjectProperty($this, 'prop'); + + $this->assertEquals('prop', $prop->getName()); + $this->assertEquals('test', $prop->getValue()); + + $prop = new \Codeception\Specify\ObjectProperty($this, 'private'); + + $this->assertEquals('private', $prop->getName()); + $this->assertEquals('private', $prop->getValue()); + + $prop = new \Codeception\Specify\ObjectProperty( + $this, new ReflectionProperty($this, 'private') + ); + + $this->assertEquals('private', $prop->getName()); + $this->assertEquals('private', $prop->getValue()); + } + + public function testRestore() + { + $this->prop = 'test'; + + $prop = new \Codeception\Specify\ObjectProperty($this, 'prop'); + $prop->setValue('another value'); + + $this->assertEquals('another value', $this->prop); + + $prop->restoreValue(); + + $this->assertEquals('test', $this->prop); + + $prop = new \Codeception\Specify\ObjectProperty($this, 'private'); + $prop->setValue('another private value'); + + $this->assertEquals('another private value', $this->private); + + $prop->restoreValue(); + + $this->assertEquals('private', $this->private); + + $prop = new \Codeception\Specify\ObjectProperty($this, 'prop', 'testing'); + + $this->assertEquals('test', $prop->getValue()); + + $prop->setValue('Hello, World!'); + + $this->assertEquals($prop->getValue(), $this->prop); + $this->assertEquals('Hello, World!', $prop->getValue()); + + $prop->restoreValue(); + + $this->assertEquals($prop->getValue(), $this->prop); + $this->assertEquals('testing', $prop->getValue()); + } +} diff --git a/tests/SpecifyTest.php b/tests/SpecifyTest.php index 0d91c9a..14407b9 100644 --- a/tests/SpecifyTest.php +++ b/tests/SpecifyTest.php @@ -279,6 +279,24 @@ public function testExamplesIndexInName() }); } + public function testMockObjectsIsolation() + { + $mock = $this->getMock(get_class($this), ['testMockObjectsIsolation']); + $mock->expects($this->once())->method('testMockObjectsIsolation'); + + $this->specify('this should fail', function () { + $mock = $this->getMock(get_class($this), ['testMockObjectsIsolation']); + $mock->expects($this->exactly(100500))->method('testMockObjectsIsolation'); + }, ['throws' => 'PHPUnit_Framework_ExpectationFailedException']); + + $this->specify('this should not fail', function () { + $mock = $this->getMock(get_class($this), ['testMockObjectsIsolation']); + $mock->expects($this->never())->method('testMockObjectsIsolation'); + }); + + $mock->testMockObjectsIsolation(); + } + // public function testFail() // { // $this->specify('this will fail', function(){