Skip to content

Commit

Permalink
Code quality, fix bug, added chained methods, added new BDD methods. (#…
Browse files Browse the repository at this point in the history
…51)

* improved code quality, fixed incomplete bug test, added chained methods, added new BDD methods

* Fix markTestIncomplete at empty 'it' or 'should' feature.

* Rename SpecifyBoostrap.php to SpecifyHooks.php
  • Loading branch information
Gustavo Nieves committed Aug 27, 2020
1 parent fe4825c commit 9848be0
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 209 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
vendor
.idea
.phpunit.result.cache
composer.phar
4 changes: 4 additions & 0 deletions composer.json
Expand Up @@ -7,6 +7,10 @@
{
"name": "Michael Bodnarchuk",
"email": "davert@codeception.com"
},
{
"name": "Gustavo Nieves",
"homepage": "https://medium.com/@ganieves"
}
],
"require": {
Expand Down
232 changes: 39 additions & 193 deletions src/Codeception/Specify.php
@@ -1,224 +1,70 @@
<?php
<?php declare(strict_types=1);

namespace Codeception;

use Codeception\Specify\SpecifyTest;
use Codeception\Specify\ObjectProperty;
use PHPUnit\Framework\AssertionFailedError;
use Closure;
use Codeception\Specify\SpecifyHooks;
use PHPUnit\Framework\TestCase;

trait Specify
{
private $beforeSpecify = array();
private $afterSpecify = array();

/**
* @var \DeepCopy\DeepCopy()
*/
private $copier;

/**
* @var SpecifyTest
*/
private $currentSpecifyTest;

private $specifyName = '';

/**
* @return SpecifyTest
*/
public function getCurrentSpecifyTest()
{
return $this->currentSpecifyTest;
}

public function should($specification, \Closure $callable = null, $params = [])
{
$this->specify("should " . $specification, $callable, $params);
}

public function it($specification, \Closure $callable = null, $params = [])
{
$this->specify($specification, $callable, $params);
}

public function describe($specification, \Closure $callable = null)
{
$this->specify($specification, $callable);
}

public function specify($specification, \Closure $callable = null, $params = [])
{
if (!$callable) {
return;
}

/** @var $this TestCase **/
if (!$this->copier) {
$this->copier = new \DeepCopy\DeepCopy();
$this->copier->skipUncloneable();
}

$properties = $this->getSpecifyObjectProperties();

// prepare for execution
$examples = $this->getSpecifyExamples($params);
$showExamplesIndex = $examples !== [[]];

$specifyName = $this->specifyName;
$this->specifyName .= ' ' . $specification;

foreach ($examples as $idx => $example) {
$test = new SpecifyTest($callable->bindTo($this));
$this->currentSpecifyTest = $test;
$test->setName($this->getName() . ' |' . $this->specifyName);
$test->setExample($example);
if ($showExamplesIndex) {
$test->setName($this->getName() . ' |' . $this->specifyName . ' # example ' . $idx);
}

// copy current object properties
$this->specifyCloneProperties($properties);

if (!empty($this->beforeSpecify) && is_array($this->beforeSpecify)) {
foreach ($this->beforeSpecify as $closure) {
if ($closure instanceof \Closure) $closure->__invoke();
}
}

$test->run($this->getTestResultObject());
$this->specifyCheckMockObjects();

// restore object properties
$this->specifyRestoreProperties($properties);

if (!empty($this->afterSpecify) && is_array($this->afterSpecify)) {
foreach ($this->afterSpecify as $closure) {
if ($closure instanceof \Closure) $closure->__invoke();
}
}
}

// revert specify name
$this->specifyName = $specifyName;
}

/**
* @param $params
* @return array
* @throws \RuntimeException
*/
private function getSpecifyExamples($params)
{
if (isset($params['examples'])) {
if (!is_array($params['examples'])) throw new \RuntimeException("Examples should be an array");
return $params['examples'];
}
return [[]];
use SpecifyHooks {
afterSpecify as public;
beforeSpecify as public;
cleanSpecify as public;
getCurrentSpecifyTest as public;
}

/**
* @return \ReflectionClass|null
*/
private function specifyGetPhpUnitReflection()
public function specify(string $thing, Closure $code = null, $examples = []): ?self
{
if ($this instanceof \PHPUnit\Framework\TestCase) {
return new \ReflectionClass(\PHPUnit\Framework\TestCase::class);
if ($code instanceof Closure) {
$this->runSpec($thing, $code, $examples);
return null;
}
return $this;
}

private function specifyCheckMockObjects()
public function describe(string $feature, Closure $code = null): ?self
{
if (($phpUnitReflection = $this->specifyGetPhpUnitReflection()) !== null) {
$verifyMockObjects = $phpUnitReflection->getMethod('verifyMockObjects');
$verifyMockObjects->setAccessible(true);
$verifyMockObjects->invoke($this);
if ($code instanceof Closure) {
$this->runSpec($feature, $code);
return null;
}
return $this;
}

function beforeSpecify(\Closure $callable = null)
{
$this->beforeSpecify[] = $callable->bindTo($this);
}

function afterSpecify(\Closure $callable = null)
{
$this->afterSpecify[] = $callable->bindTo($this);
}

function cleanSpecify()
public function it(string $specification, Closure $code = null, $examples = []): self
{
$this->beforeSpecify = $this->afterSpecify = array();
}

/**
* @param ObjectProperty[] $properties
*/
private function specifyRestoreProperties($properties)
{
foreach ($properties as $property) {
$property->restoreValue();
if ($code instanceof Closure) {
$this->runSpec($specification, $code, $examples);
return $this;
}
TestCase::markTestIncomplete();
return $this;
}

/**
* @return ObjectProperty[]
*/
private function getSpecifyObjectProperties()
public function its(string $specification, Closure $code = null, $examples = []): self
{
$objectReflection = new \ReflectionObject($this);
$properties = $objectReflection->getProperties();

if (($classProperties = $this->specifyGetClassPrivateProperties()) !== []) {
$properties = array_merge($properties, $classProperties);
}

$clonedProperties = [];

foreach ($properties as $property) {
/** @var $property \ReflectionProperty **/
$docBlock = $property->getDocComment();
if (!$docBlock) {
continue;
}
if (preg_match('~\*(\s+)?@specify\s?~', $docBlock)) {
$property->setAccessible(true);
$clonedProperties[] = new ObjectProperty($this, $property);
}
}

// isolate mockObjects property from PHPUnit\Framework\TestCase
if ($classReflection = $this->specifyGetPhpUnitReflection()) {
$property = $classReflection->getProperty('mockObjects');
// remove all mock objects inherited from parent scope(s)
$clonedProperties[] = new ObjectProperty($this, $property);
$property->setValue($this, []);
}

return $clonedProperties;
return $this->it($specification, $code, $examples);
}

private function specifyGetClassPrivateProperties()
public function should(string $behavior, Closure $code = null, $examples = []): self
{
static $properties = [];

if (!isset($properties[__CLASS__])) {
$reflection = new \ReflectionClass(__CLASS__);

$properties[__CLASS__] = (get_class($this) !== __CLASS__)
? $reflection->getProperties(\ReflectionProperty::IS_PRIVATE) : [];
if ($code instanceof Closure) {
$this->runSpec('should ' . $behavior, $code, $examples);
return $this;
}

return $properties[__CLASS__];
TestCase::markTestIncomplete();
return $this;
}

/**
* @param ObjectProperty[] $properties
*/
private function specifyCloneProperties($properties)
public function shouldNot(string $behavior, Closure $code = null, $examples = []): self
{
foreach ($properties as $property) {
$propertyValue = $property->getValue();
$property->setValue($this->copier->copy($propertyValue));
if ($code instanceof Closure) {
$this->runSpec('should not ' . $behavior, $code, $examples);
return $this;
}
TestCase::markTestIncomplete();
return $this;
}
}
9 changes: 6 additions & 3 deletions src/Codeception/Specify/ObjectProperty.php
@@ -1,6 +1,9 @@
<?php

namespace Codeception\Specify;

use ReflectionProperty;

/**
* Helper for manipulating by an object property.
*
Expand All @@ -14,7 +17,7 @@ class ObjectProperty
private $owner;

/**
* @var \ReflectionProperty|string
* @var ReflectionProperty|string
*/
private $property;

Expand All @@ -35,8 +38,8 @@ 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);
if (!($this->property instanceof ReflectionProperty)) {
$this->property = new ReflectionProperty($owner, $this->property);
}

$this->property->setAccessible(true);
Expand Down

0 comments on commit 9848be0

Please sign in to comment.