├── VERSION ├── .gitignore ├── tests ├── _bootstrap.php ├── _support │ └── SpecifyUnitTest.php ├── ObjectPropertyTest.php └── SpecifyTest.php ├── phpunit.xml ├── .github └── workflows │ └── main.yml ├── src └── Codeception │ ├── Specify │ ├── ResultPrinter.php │ ├── ObjectProperty.php │ ├── SpecifyTest.php │ └── SpecifyHooks.php │ └── Specify.php ├── composer.json ├── LICENSE ├── CHANGELOG.md └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 1.5.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | .phpunit.result.cache 4 | composer.lock 5 | composer.phar -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | tests 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/_support/SpecifyUnitTest.php: -------------------------------------------------------------------------------- 1 | private = $private; 22 | } 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | protected function getPrivateProperty() 28 | { 29 | return $this->private; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | php: [7.4, 8.0, 8.1] 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php }} 21 | 22 | - name: Validate composer.json 23 | run: composer validate 24 | 25 | - name: Install dependencies 26 | run: composer update --prefer-dist --no-progress --no-interaction 27 | 28 | - name: Run test suite 29 | run: php vendor/bin/phpunit 30 | -------------------------------------------------------------------------------- /src/Codeception/Specify/ResultPrinter.php: -------------------------------------------------------------------------------- 1 | write($progress); 18 | ++$this->column; 19 | ++$this->numTestsRun; 20 | 21 | if ($this->column === $this->maxColumn) { 22 | $this->writeNewLine(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeception/specify", 3 | "description": "BDD code blocks for PHPUnit and Codeception", 4 | "minimum-stability": "stable", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Michael Bodnarchuk", 9 | "email": "davert@codeception.com" 10 | }, 11 | { 12 | "name": "Gustavo Nieves", 13 | "homepage": "https://medium.com/@ganieves" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=7.4.0", 18 | "myclabs/deep-copy": "^1.10", 19 | "phpunit/phpunit": "^8.0|^9.0" 20 | }, 21 | "autoload": { 22 | "psr-0": { 23 | "Codeception\\": "src/" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2022 Codeception PHP Testing Framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/Codeception/Specify/ObjectProperty.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ObjectProperty 15 | { 16 | /** 17 | * @var mixed 18 | */ 19 | private $owner; 20 | 21 | /** 22 | * @var ReflectionProperty|string 23 | */ 24 | private $property; 25 | 26 | /** 27 | * @var mixed 28 | */ 29 | private $initValue; 30 | 31 | /** 32 | * ObjectProperty constructor. 33 | * 34 | * @param $owner 35 | * @param $property 36 | * @param $value 37 | */ 38 | public function __construct($owner, $property, $value = null) 39 | { 40 | $this->owner = $owner; 41 | $this->property = $property; 42 | 43 | if (!($this->property instanceof ReflectionProperty)) { 44 | $this->property = new ReflectionProperty($owner, $this->property); 45 | } 46 | 47 | $this->property->setAccessible(true); 48 | 49 | $this->initValue = ($value ?? $this->getValue()); 50 | } 51 | 52 | public function getName(): string 53 | { 54 | return $this->property->getName(); 55 | } 56 | 57 | /** 58 | * Restores initial value 59 | */ 60 | public function restoreValue(): void 61 | { 62 | $this->setValue($this->initValue); 63 | } 64 | 65 | /** 66 | * @return mixed 67 | */ 68 | public function getValue() 69 | { 70 | return $this->property->getValue($this->owner); 71 | } 72 | 73 | /** 74 | * @param mixed $value 75 | */ 76 | public function setValue($value): void 77 | { 78 | $this->property->setValue($this->owner, $value); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.5 4 | 5 | * Removed support for `PHP 7.1` and `PHP 7.2`. 6 | * Use strict types in the source code. 7 | 8 | ## 1.4 9 | 10 | * Added **_Fluent Interface_** support, this allows you to add `it`'s and `should`'s chained to a `specify` or `describe`. 11 | * `Specify.php` trait now only has the public API methods. 12 | * If an `it` or `should` only receives text now that test is marked as incomplete. 13 | * `shouldNot` and `its` were added as aliases. 14 | * Added `.phpunit.result.cache` file to `.gitignore` 15 | * Updated `README.md` 16 | 17 | ## 1.0 18 | 19 | * BREAKING: PHPUnit 6 support 20 | * BREAKING: **Removed configuration** section 21 | * BREAKING: **Only properties marked with `@specify` annotation are cloned** in specify blocks. 22 | * BREAKING: **Removed throws** parameter in specify blocks 23 | * Added `Codeception\Specify\ResultPrinter` to fix printing progress of specify blocks. 24 | 25 | ### Upgrade Plan 26 | 27 | 1. Update to PHP7+ PHPUnit 6+ 28 | 2. Add to `phpunit.xml`: `printerClass="Codeception\Specify\ResultPrinter"` 29 | 3. If relied on property cloning, add `@specify` annotation for all properties which needs to be cloned for specify blocks 30 | 4. If you used `throws` parameter, consider using [AssertThrows](https://github.com/Codeception/AssertThrows) package. 31 | 32 | ## 0.4.3 33 | 34 | * Show example index on failure by @zszucs *2015-11-27* 35 | 36 | 37 | ## 0.4.2 38 | 39 | * Testing exception messages by @chrismichaels84 https://github.com/Codeception/Specify#exceptions 40 | 41 | ## 0.4.0 42 | 43 | * Fixes cloning properties in examples. Issue #6 *2014-10-15* 44 | * Added global and local specify configs, for disabling cloning properties and changing cloning methods *2014-10-15* 45 | 46 | 47 | ## 0.3.6 48 | #### 03/22/2014 49 | * Cloning unclonnable items 50 | 51 | 52 | ## 0.3.5 53 | #### 03/22/2014 54 | 55 | * Updated to DeepCopy 1.1.0 56 | 57 | 58 | ## 0.3.4 59 | #### 02/23/2014 60 | 61 | * Added DeepCopy library to save/restore objects between specs 62 | * Robo file for releases 63 | -------------------------------------------------------------------------------- /tests/ObjectPropertyTest.php: -------------------------------------------------------------------------------- 1 | prop = 'test'; 16 | 17 | $prop = new ObjectProperty($this, 'prop'); 18 | 19 | $this->assertEquals('prop', $prop->getName()); 20 | $this->assertEquals('test', $prop->getValue()); 21 | 22 | $prop = new ObjectProperty($this, 'private'); 23 | 24 | $this->assertEquals('private', $prop->getName()); 25 | $this->assertEquals('private', $prop->getValue()); 26 | 27 | $prop = new ObjectProperty( 28 | $this, new ReflectionProperty($this, 'private') 29 | ); 30 | 31 | $this->assertEquals('private', $prop->getName()); 32 | $this->assertEquals('private', $prop->getValue()); 33 | } 34 | 35 | public function testRestore() 36 | { 37 | $this->prop = 'test'; 38 | 39 | $prop = new ObjectProperty($this, 'prop'); 40 | $prop->setValue('another value'); 41 | 42 | $this->assertEquals('another value', $this->prop); 43 | 44 | $prop->restoreValue(); 45 | 46 | $this->assertEquals('test', $this->prop); 47 | 48 | $prop = new ObjectProperty($this, 'private'); 49 | $prop->setValue('another private value'); 50 | 51 | $this->assertEquals('another private value', $this->private); 52 | 53 | $prop->restoreValue(); 54 | 55 | $this->assertEquals('private', $this->private); 56 | 57 | $prop = new ObjectProperty($this, 'prop', 'testing'); 58 | 59 | $this->assertEquals('test', $prop->getValue()); 60 | 61 | $prop->setValue('Hello, World!'); 62 | 63 | $this->assertEquals($prop->getValue(), $this->prop); 64 | $this->assertEquals('Hello, World!', $prop->getValue()); 65 | 66 | $prop->restoreValue(); 67 | 68 | $this->assertEquals($prop->getValue(), $this->prop); 69 | $this->assertEquals('testing', $prop->getValue()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Codeception/Specify.php: -------------------------------------------------------------------------------- 1 | runSpec($thing, $code, $examples); 24 | return null; 25 | } 26 | 27 | return $this; 28 | } 29 | 30 | public function describe(string $feature, Closure $code = null): ?self 31 | { 32 | if ($code instanceof Closure) { 33 | $this->runSpec($feature, $code); 34 | return null; 35 | } 36 | 37 | return $this; 38 | } 39 | 40 | public function it(string $specification, Closure $code = null, $examples = []): self 41 | { 42 | if ($code instanceof Closure) { 43 | $this->runSpec($specification, $code, $examples); 44 | return $this; 45 | } 46 | 47 | TestCase::markTestIncomplete(); 48 | return $this; 49 | } 50 | 51 | public function its(string $specification, Closure $code = null, $examples = []): self 52 | { 53 | return $this->it($specification, $code, $examples); 54 | } 55 | 56 | public function should(string $behavior, Closure $code = null, $examples = []): self 57 | { 58 | if ($code instanceof Closure) { 59 | $this->runSpec('should ' . $behavior, $code, $examples); 60 | return $this; 61 | } 62 | 63 | TestCase::markTestIncomplete(); 64 | return $this; 65 | } 66 | 67 | public function shouldNot(string $behavior, Closure $code = null, $examples = []): self 68 | { 69 | if ($code instanceof Closure) { 70 | $this->runSpec('should not ' . $behavior, $code, $examples); 71 | return $this; 72 | } 73 | 74 | TestCase::markTestIncomplete(); 75 | return $this; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Codeception/Specify/SpecifyTest.php: -------------------------------------------------------------------------------- 1 | test = $test; 30 | } 31 | 32 | public function setName(string $name): void 33 | { 34 | $this->name = $name; 35 | } 36 | 37 | public function toString(): string 38 | { 39 | return $this->name; 40 | } 41 | 42 | public function getName($withDataSet = true): string 43 | { 44 | if ($withDataSet && !empty($this->example)) { 45 | $exporter = new Exporter(); 46 | return $this->name . ' | ' . $exporter->shortenedRecursiveExport($this->example); 47 | } 48 | 49 | return $this->name; 50 | } 51 | 52 | /** 53 | * Count elements of an object 54 | * @link http://php.net/manual/en/countable.count.php 55 | * @return int The custom count as an integer. 56 | * 57 | * The return value is cast to an integer. 58 | * @since 5.1.0 59 | */ 60 | public function count(): int 61 | { 62 | return 1; 63 | } 64 | 65 | /** 66 | * Runs a test and collects its result in a TestResult instance. 67 | */ 68 | public function run(TestResult $result = null): TestResult 69 | { 70 | try { 71 | call_user_func_array($this->test, $this->example); 72 | } catch (AssertionFailedError $error) { 73 | $result->addFailure(clone($this), $error, $result->time()); 74 | } 75 | 76 | return $result; 77 | } 78 | 79 | public function setExample(array $example): void 80 | { 81 | $this->example = $example; 82 | } 83 | 84 | /** 85 | * @param mixed $throws 86 | */ 87 | public function setThrows($throws): void 88 | { 89 | $this->throws = $throws; 90 | } 91 | } -------------------------------------------------------------------------------- /src/Codeception/Specify/SpecifyHooks.php: -------------------------------------------------------------------------------- 1 | currentSpecifyTest; 31 | } 32 | 33 | /** 34 | * @param string $specification 35 | * @param Closure|null $callable 36 | * @param callable|array $params 37 | */ 38 | private function runSpec(string $specification, Closure $callable = null, $params = []) 39 | { 40 | if ($callable === null) { 41 | return; 42 | } 43 | 44 | if (!$this->copier) { 45 | $this->copier = new DeepCopy(); 46 | $this->copier->skipUncloneable(); 47 | } 48 | 49 | $properties = $this->getSpecifyObjectProperties(); 50 | 51 | // prepare for execution 52 | $examples = $this->getSpecifyExamples($params); 53 | $showExamplesIndex = $examples !== [[]]; 54 | 55 | $specifyName = $this->specifyName; 56 | $this->specifyName .= ' ' . $specification; 57 | 58 | foreach ($examples as $idx => $example) { 59 | $test = new SpecifyTest($callable->bindTo($this)); 60 | $this->currentSpecifyTest = $test; 61 | $test->setName($this->getName() . ' |' . $this->specifyName); 62 | $test->setExample($example); 63 | if ($showExamplesIndex) { 64 | $test->setName($this->getName() . ' |' . $this->specifyName . ' # example ' . $idx); 65 | } 66 | 67 | // copy current object properties 68 | $this->specifyCloneProperties($properties); 69 | 70 | if (!empty($this->beforeSpecify) && is_array($this->beforeSpecify)) { 71 | foreach ($this->beforeSpecify as $closure) { 72 | if ($closure instanceof Closure) $closure->__invoke(); 73 | } 74 | } 75 | 76 | $test->run($this->getTestResultObject()); 77 | $this->specifyCheckMockObjects(); 78 | 79 | // restore object properties 80 | $this->specifyRestoreProperties($properties); 81 | 82 | if (!empty($this->afterSpecify) && is_array($this->afterSpecify)) { 83 | foreach ($this->afterSpecify as $closure) { 84 | if ($closure instanceof Closure) $closure->__invoke(); 85 | } 86 | } 87 | } 88 | 89 | // revert specify name 90 | $this->specifyName = $specifyName; 91 | } 92 | 93 | /** 94 | * @param $params 95 | * @return array 96 | */ 97 | private function getSpecifyExamples($params): array 98 | { 99 | if (isset($params['examples'])) { 100 | if (!is_array($params['examples'])) { 101 | throw new RuntimeException("Examples should be an array"); 102 | } 103 | 104 | return $params['examples']; 105 | } 106 | 107 | return [[]]; 108 | } 109 | 110 | private function specifyGetPhpUnitReflection(): ?ReflectionClass 111 | { 112 | if ($this instanceof TestCase) { 113 | return new ReflectionClass(TestCase::class); 114 | } 115 | 116 | return null; 117 | } 118 | 119 | private function specifyCheckMockObjects() 120 | { 121 | if (($phpUnitReflection = $this->specifyGetPhpUnitReflection()) !== null) { 122 | try { 123 | $verifyMockObjects = $phpUnitReflection->getMethod('verifyMockObjects'); 124 | $verifyMockObjects->setAccessible(true); 125 | $verifyMockObjects->invoke($this); 126 | } catch (ReflectionException $e) { 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * @param ObjectProperty[] $properties 133 | */ 134 | private function specifyRestoreProperties(array $properties) 135 | { 136 | foreach ($properties as $property) { 137 | $property->restoreValue(); 138 | } 139 | } 140 | 141 | /** 142 | * @return ObjectProperty[] 143 | */ 144 | private function getSpecifyObjectProperties(): array 145 | { 146 | $objectReflection = new ReflectionObject($this); 147 | $properties = $objectReflection->getProperties(); 148 | 149 | if (($classProperties = $this->specifyGetClassPrivateProperties()) !== []) { 150 | $properties = array_merge($properties, $classProperties); 151 | } 152 | 153 | $clonedProperties = []; 154 | 155 | foreach ($properties as $property) { 156 | /** @var ReflectionProperty $property **/ 157 | $docBlock = $property->getDocComment(); 158 | if (!$docBlock) { 159 | continue; 160 | } 161 | 162 | if (preg_match('#\*(\s+)?@specify\s?#', $docBlock)) { 163 | $property->setAccessible(true); 164 | $clonedProperties[] = new ObjectProperty($this, $property); 165 | } 166 | } 167 | 168 | // isolate mockObjects property from PHPUnit\Framework\TestCase 169 | if ($classReflection = $this->specifyGetPhpUnitReflection()) { 170 | try { 171 | $property = $classReflection->getProperty('mockObjects'); 172 | // remove all mock objects inherited from parent scope(s) 173 | $clonedProperties[] = new ObjectProperty($this, $property); 174 | $property->setValue($this, []); 175 | } catch (ReflectionException $e) { 176 | } 177 | } 178 | 179 | return $clonedProperties; 180 | } 181 | 182 | private function specifyGetClassPrivateProperties(): array 183 | { 184 | static $properties = []; 185 | 186 | if (!isset($properties[__CLASS__])) { 187 | $reflection = new ReflectionClass(__CLASS__); 188 | 189 | $properties[__CLASS__] = (get_class($this) !== __CLASS__) 190 | ? $reflection->getProperties(ReflectionProperty::IS_PRIVATE) : []; 191 | } 192 | 193 | return $properties[__CLASS__]; 194 | } 195 | 196 | /** 197 | * @param ObjectProperty[] $properties 198 | */ 199 | private function specifyCloneProperties(array $properties) 200 | { 201 | foreach ($properties as $property) { 202 | $propertyValue = $property->getValue(); 203 | $property->setValue($this->copier->copy($propertyValue)); 204 | } 205 | } 206 | 207 | private function beforeSpecify(Closure $callable = null) 208 | { 209 | $this->beforeSpecify[] = $callable->bindTo($this); 210 | } 211 | 212 | private function afterSpecify(Closure $callable = null) 213 | { 214 | $this->afterSpecify[] = $callable->bindTo($this); 215 | } 216 | 217 | private function cleanSpecify() 218 | { 219 | $this->afterSpecify = []; 220 | $this->beforeSpecify = []; 221 | } 222 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Specify 2 | ======= 3 | 4 | BDD style code blocks for [PHPUnit][1] or [Codeception][2] 5 | 6 | [![Latest Stable Version](https://poser.pugx.org/codeception/specify/v/stable)](https://packagist.org/packages/codeception/specify) 7 | [![Total Downloads](https://poser.pugx.org/codeception/specify/downloads)](https://packagist.org/packages/codeception/specify) 8 | [![Latest Unstable Version](https://poser.pugx.org/codeception/specify/v/unstable)](https://packagist.org/packages/codeception/specify) 9 | [![License](https://poser.pugx.org/codeception/specify/license)](https://packagist.org/packages/codeception/specify) 10 | [![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) 11 | 12 | Specify allows you to write your tests in more readable BDD style, the same way you might have experienced with [Jasmine][3]. 13 | Inspired by MiniTest of Ruby now you combine BDD and classical TDD style in one test. 14 | 15 | ## Installation 16 | 17 | *Requires PHP >= 7.4* 18 | 19 | * Install with Composer: 20 | 21 | ``` 22 | composer require codeception/specify --dev 23 | ``` 24 | 25 | * Include `Codeception\Specify` trait in your tests. 26 | 27 | 28 | ## Usage 29 | 30 | Specify `$this->specify` method to add isolated test blocks for your PHPUnit tests! 31 | 32 | ```php 33 | public function testValidation() 34 | { 35 | $this->assertInstanceOf('Model', $this->user); 36 | 37 | $this->specify('username is required', function() { 38 | $this->user->username = null; 39 | $this->assertFalse($this->user->validate(['username'])); 40 | }); 41 | 42 | $this->specify('username is too long', function() { 43 | $this->user->username = 'toolooooongnaaaaaaameeee'; 44 | $this->assertFalse($this->user->validate(['username'])); 45 | }); 46 | } 47 | ``` 48 | 49 | ### BDD Example 50 | 51 | Specify supports `describe-it` and `describe-should` BDD syntax inside PHPUnit 52 | 53 | ```php 54 | public function testValidation() 55 | { 56 | $this->describe('user', function () { 57 | $this->it('should have a name', function() { 58 | $this->user->username = null; 59 | $this->assertFalse($this->user->validate(['username'])); 60 | }); 61 | }); 62 | 63 | // you can use chained methods for better readability: 64 | $this->describe('user') 65 | ->should('be ok with valid name', function() { 66 | $this->user->username = 'davert'; 67 | $this->assertTrue($this->user->validate(['username'])); 68 | }) 69 | ->shouldNot('have long name', function() { 70 | $this->user->username = 'toolooooongnaaaaaaameeee'; 71 | $this->assertFalse($this->user->validate(['username'])); 72 | }) 73 | // empty codeblocks are marked as Incomplete tests 74 | ->it('should be ok with valid name') 75 | ; 76 | } 77 | ``` 78 | 79 | 80 | ### Specify + Verify Example 81 | 82 | Use [Codeception/Verify][4] for simpler assertions: 83 | 84 | ```php 85 | public function testValidation() 86 | { 87 | $this->specify('username is too long', function() { 88 | $this->user->username = 'toolooooongnaaaaaaameeee'; 89 | expect_not($this->user->validate(['username'])); 90 | }); 91 | 92 | $this->specify('username is ok', function() { 93 | $this->user->username = 'davert'; 94 | expect_that($this->user->validate(['username'])); 95 | }); 96 | } 97 | ``` 98 | 99 | ## Use Case 100 | 101 | This tiny library makes your tests readable by organizing them in nested code blocks. 102 | This allows to combine similar tests into one but put them inside nested sections. 103 | 104 | This is very similar to BDD syntax as in RSpec or Mocha but works inside PHPUnit: 105 | 106 | ```php 107 | user = new User; 119 | } 120 | 121 | public function testValidation() 122 | { 123 | $this->user->name = 'davert'; 124 | $this->specify('i can change my name', function() { 125 | $this->user->name = 'jon'; 126 | $this->assertEquals('jon', $this->user->name); 127 | }); 128 | // user name is 'davert' again 129 | $this->assertEquals('davert', $this->user->name); 130 | } 131 | } 132 | ``` 133 | 134 | Each code block is isolated. This means call to `$this->specify` does not change values of properties of a test class. 135 | Isolated properties should be marked with `@specify` annotation. 136 | 137 | Failure in `specify` block won't get your test stopped. 138 | 139 | ```php 140 | specify('failing but test goes on', function() { 142 | $this->fail('bye'); 143 | }); 144 | $this->assertTrue(true); 145 | 146 | // Assertions: 2, Failures: 1 147 | ?> 148 | ``` 149 | 150 | If a test fails you will see specification text in the result. 151 | 152 | ## Isolation 153 | 154 | Isolation is achieved by **cloning object properties** for each specify block. 155 | Only properties marked with `@specify` annotation are cloned. 156 | 157 | ```php 158 | /** @specify */ 159 | protected $user; // cloning 160 | 161 | /** 162 | * @specify 163 | **/ 164 | protected $user; // cloning 165 | 166 | protected $repository; // not cloning 167 | ``` 168 | 169 | Objects are cloned using deep cloning method. 170 | 171 | **If object cloning affects performance, consider turning the clonning off**. 172 | 173 | **Mocks are isolated** by default. 174 | 175 | A mock defined inside a specify block won't be executed inside an outer test, 176 | and mock from outer test won't be triggered inside codeblock. 177 | 178 | ```php 179 | createMock(Config::class); 181 | $config->expects($this->once())->method('init'); 182 | 183 | $config->init(); 184 | // success: $config->init() was executed 185 | 186 | $this->specify('this should not fail', function () { 187 | $config = $this->createMock(Config::class); 188 | $config->expects($this->never())->method('init')->willReturn(null); 189 | // success: $config->init() is never executed 190 | }); 191 | ``` 192 | 193 | ## Examples: DataProviders alternative 194 | 195 | ```php 196 | specify('should calculate square numbers', function($number, $square) { 198 | $this->assertEquals($square, $number*$number); 199 | }, ['examples' => [ 200 | [2,4], 201 | [3,9] 202 | ]]); 203 | ``` 204 | 205 | You can also use DataProvider functions in `examples` param. 206 | 207 | ```php 208 | specify('should calculate square numbers', function($number, $square) { 210 | $this->assertEquals($square, $number*$number); 211 | }, ['examples' => $this->provider()]); 212 | ``` 213 | 214 | Can also be used with real data providers: 215 | 216 | ```php 217 | specify('should assert data provider', function ($example) use ($param) { 224 | $this->assertGreaterThanOrEqual(5, $param + $example); 225 | }, ['examples' => [[4], [7], [5]]]); 226 | } 227 | 228 | public function someData() 229 | { 230 | return [[1], [2]]; 231 | } 232 | ``` 233 | 234 | ## Before/After 235 | 236 | There are also before and after callbacks, which act as setUp/tearDown but for specify. 237 | 238 | ```php 239 | beforeSpecify(function() { 241 | // prepare something; 242 | }); 243 | $this->afterSpecify(function() { 244 | // reset something 245 | }); 246 | $this->cleanSpecify(); // removes before/after callbacks 247 | ?> 248 | ``` 249 | 250 | ## API 251 | 252 | Available methods: 253 | 254 | ```php 255 | // Starts a specify code block: 256 | $this->specify(string $thing, callable $code = null, $examples = []) 257 | 258 | // Starts a describe code block. Same as 'specify' but expects chained 'it' or 'should' methods. 259 | $this->describe(string $feature, callable $code = null) 260 | 261 | // Starts a code block. If 'code' is null, marks test as incomplete. 262 | $this->it(string $spec, callable $code = null, $examples = []) 263 | $this->its(string $spec, callable $code = null, $examples = []) 264 | 265 | // Starts a code block. Same as 'it' but prepends 'should' or 'should not' into description. 266 | $this->should(string $behavior, callable $code = null, $examples = []) 267 | $this->shouldNot(string $behavior, callable $code = null, $examples = []) 268 | ``` 269 | 270 | ## Printer Options 271 | 272 | For PHPUnit, add `Codeception\Specify\ResultPrinter` printer into `phpunit.xml` 273 | 274 | ```xml 275 | 276 | 277 | ``` 278 | 279 | ## Recommended 280 | 281 | * Use [Codeception/AssertThrows][5] for exception assertions 282 | * Use [Codeception/DomainAssert][6] for verbose domain logic assertions 283 | * Combine this with [Codeception/Verify][4] library, to get BDD style assertions. 284 | 285 | License: [MIT.][7] 286 | 287 | [1]: https://phpunit.de/ 288 | [2]: https://codeception.com/ 289 | [3]: https://jasmine.github.io/ 290 | [4]: https://github.com/Codeception/Verify 291 | [5]: https://github.com/Codeception/AssertThrows 292 | [6]: https://github.com/Codeception/DomainAssert 293 | [7]: /LICENSE 294 | -------------------------------------------------------------------------------- /tests/SpecifyTest.php: -------------------------------------------------------------------------------- 1 | user = new User(); 32 | $this->user->name = 'davert'; 33 | $this->specify("i can change my name", function() { 34 | $this->user->name = 'jon'; 35 | $this->assertEquals('jon', $this->user->name); 36 | }); 37 | 38 | $this->assertEquals('davert', $this->user->name); 39 | 40 | try { 41 | $this->specify('i can fail here but test goes on', function() { 42 | $this->markTestIncomplete(); 43 | }); 44 | } catch (IncompleteTestError $error) { 45 | $this->fail("should not be thrown"); 46 | } 47 | 48 | $this->assertTrue(true); 49 | } 50 | 51 | public function testBeforeCallback() 52 | { 53 | $this->beforeSpecify(function() { 54 | $this->user = "davert"; 55 | }); 56 | $this->specify("user should be davert", function() { 57 | $this->assertEquals('davert', $this->user); 58 | }); 59 | } 60 | 61 | public function testMultiBeforeCallback() 62 | { 63 | $this->beforeSpecify(function() { 64 | $this->user = "davert"; 65 | }); 66 | $this->beforeSpecify(function() { 67 | $this->user .= "jon"; 68 | }); 69 | $this->specify("user should be davertjon", function() { 70 | $this->assertEquals('davertjon', $this->user); 71 | }); 72 | } 73 | 74 | public function testAfterCallback() 75 | { 76 | $this->afterSpecify(function() { 77 | $this->user = "davert"; 78 | }); 79 | $this->specify("user should be davert", function() { 80 | $this->user = "jon"; 81 | }); 82 | $this->assertEquals('davert', $this->user); 83 | } 84 | 85 | public function testMultiAfterCallback() 86 | { 87 | $this->afterSpecify(function() { 88 | $this->user = "davert"; 89 | }); 90 | $this->afterSpecify(function() { 91 | $this->user .= "jon"; 92 | }); 93 | $this->specify("user should be davertjon", function() { 94 | $this->user = "jon"; 95 | }); 96 | $this->assertEquals('davertjon', $this->user); 97 | } 98 | 99 | public function testCleanSpecifyCallbacks() 100 | { 101 | $this->afterSpecify(function() { 102 | $this->user = "davert"; 103 | }); 104 | $this->cleanSpecify(); 105 | $this->specify("user should be davert", function() { 106 | $this->user = "jon"; 107 | }); 108 | $this->assertNull($this->user); 109 | } 110 | 111 | public function testExamples() 112 | { 113 | $this->specify('specify may contain examples', function($a, $b) { 114 | $this->assertEquals($b, $a*$a); 115 | }, ['examples' => [ 116 | ['2', '4'], 117 | ['3', '9'] 118 | ]]); 119 | } 120 | 121 | public function testOnlySpecifications() 122 | { 123 | $this->specify('should be valid'); 124 | $this->assertTrue(true); 125 | } 126 | 127 | public function testDeepCopy() 128 | { 129 | $this->a = new TestOne(); 130 | $this->a->prop = new TestOne(); 131 | $this->a->prop->prop = 1; 132 | $this->specify('nested object can be changed', function() { 133 | $this->assertEquals(1, $this->a->prop->prop); 134 | $this->a->prop->prop = 2; 135 | $this->assertEquals(2, $this->a->prop->prop); 136 | }); 137 | $this->assertEquals(1, $this->a->prop->prop); 138 | 139 | } 140 | 141 | public function testDeepRevert() 142 | { 143 | $this->specify("user should be jon", function() { 144 | $this->user = "jon"; 145 | }); 146 | 147 | $this->specify("user should be davert", function() { 148 | $this->user = "davert"; 149 | }); 150 | 151 | $this->a = new TestOne(); 152 | $this->a->prop = new TestOne(); 153 | $this->a->prop->prop = 1; 154 | 155 | $this->specify("user should be davert", function() { 156 | $this->a->prop->prop = "davert"; 157 | }); 158 | 159 | $this->assertEquals(1, $this->a->prop->prop); 160 | } 161 | 162 | public function testCloneOnlySpecified() 163 | { 164 | $this->user = "bob"; 165 | $this->b = "rob"; 166 | $this->specify("user should be jon", function() { 167 | $this->user = "jon"; 168 | $this->b = 'alice'; 169 | }); 170 | $this->assertEquals('bob', $this->user); 171 | $this->assertEquals('alice', $this->b); 172 | } 173 | 174 | 175 | // public function testFail() 176 | // { 177 | // $this->specify('this will fail', function(){ 178 | // $this->assertTrue(false); 179 | // }); 180 | // 181 | // $this->specify('this will fail', function(){ 182 | // $this->assertTrue(false); 183 | // }); 184 | // 185 | // $this->specify('this will fail', function(){ 186 | // $this->assertTrue(false); 187 | // }); 188 | // 189 | // $this->specify('this will fail', function(){ 190 | // $this->assertTrue(false); 191 | // }); 192 | // $this->specify('this will fail', function(){ 193 | // $this->assertTrue(false); 194 | // }); 195 | // $this->specify('this will fail', function(){ 196 | // $this->assertTrue(false); 197 | // }); 198 | // 199 | // 200 | // $this->specify('this will fail too', function(){ 201 | // $this->assertTrue(true); 202 | // }, ['throws' => 'Exception']); 203 | // } 204 | 205 | 206 | /** 207 | * @Issue https://github.com/Codeception/Specify/issues/6 208 | */ 209 | public function testPropertyRestore() 210 | { 211 | $this->a = new TestOne(); 212 | $this->a->prop = ['hello', 'world']; 213 | 214 | $this->specify('array contains hello+world', function ($testData) { 215 | $this->a->prop = ['bye', 'world']; 216 | $this->assertContains($testData, $this->a->prop); 217 | }, ['examples' => [ 218 | ['bye'], 219 | ['world'], 220 | ]]); 221 | 222 | $this->assertEquals(['hello', 'world'], $this->a->prop); 223 | $this->assertFalse($this->private); 224 | $this->assertTrue($this->getPrivateProperty()); 225 | 226 | $this->specify('property $private should be restored properly', function() { 227 | $this->private = "i'm protected"; 228 | $this->setPrivateProperty("i'm private"); 229 | $this->assertEquals("i'm private", $this->getPrivateProperty()); 230 | }); 231 | 232 | $this->assertFalse($this->private); 233 | $this->assertTrue($this->getPrivateProperty()); 234 | } 235 | 236 | public function testExamplesIndexInName() 237 | { 238 | $name = $this->getName(); 239 | 240 | $this->specify('it appends index of an example to a test case name', function ($idx) use ($name) { 241 | $name .= ' | it appends index of an example to a test case name'; 242 | $this->assertEquals($name . ' # example ' . $idx, $this->getCurrentSpecifyTest()->getName(false)); 243 | }, ['examples' => [ 244 | [0, ''], 245 | [1, '0'], 246 | [2, null], 247 | [3, 'bye'], 248 | [4, 'world'], 249 | ]]); 250 | 251 | $this->specify('it does not append index to a test case name if there are no examples', function () use ($name) { 252 | $name .= ' | it does not append index to a test case name if there are no examples'; 253 | $this->assertEquals($name, $this->getCurrentSpecifyTest()->getName(false)); 254 | 255 | $this->specify('nested specification without examples', function () use ($name) { 256 | $this->assertEquals($name . ' nested specification without examples', $this->getCurrentSpecifyTest()->getName(false)); 257 | }); 258 | 259 | $this->specify('nested specification with examples', function () use ($name) { 260 | $this->assertEquals($name . ' nested specification with examples # example 0', $this->getCurrentSpecifyTest()->getName(false)); 261 | }, ['examples' => [ 262 | [null] 263 | ]]); 264 | }); 265 | } 266 | 267 | public function testNestedSpecify() 268 | { 269 | $name = $this->getName(); 270 | 271 | $this->specify('user', function() use ($name) { 272 | $name .= ' | user'; 273 | $this->specify('nested specification', function () use ($name) { 274 | $name .= ' nested specification'; 275 | $this->assertEquals($name, $this->getCurrentSpecifyTest()->getName(false)); 276 | }); 277 | 278 | }); 279 | } 280 | 281 | public function testBDDStyle() 282 | { 283 | $name = $this->getName(); 284 | 285 | $this->describe('user', function() use ($name) { 286 | $name .= ' | user'; 287 | $this->it('should be ok', function () use ($name) { 288 | $name .= ' should be ok'; 289 | $this->assertEquals($name, $this->getCurrentSpecifyTest()->getName(false)); 290 | }); 291 | $this->should('be ok', function () use ($name) { 292 | $name .= ' should be ok'; 293 | $this->assertEquals($name, $this->getCurrentSpecifyTest()->getName(false)); 294 | }); 295 | }); 296 | } 297 | 298 | public function testMockObjectsIsolation() 299 | { 300 | $mock = $this->createMock(get_class($this)); 301 | $mock->expects($this->once())->method('testMockObjectsIsolation'); 302 | $mock->testMockObjectsIsolation(); 303 | 304 | $this->specify('this should not fail', function () { 305 | $mock = $this->createMock(get_class($this)); 306 | $mock->expects($this->never())->method('testMockObjectsIsolation')->willReturn(null); 307 | }); 308 | 309 | } 310 | 311 | /** 312 | * @dataProvider someData 313 | */ 314 | public function testSpecifyAndDataProvider(int $param) 315 | { 316 | $this->specify('should assert data provider', function () use ($param) { 317 | $this->assertGreaterThan(0, $param); 318 | }); 319 | } 320 | 321 | /** 322 | * @dataProvider someData 323 | */ 324 | public function testExamplesAndDataProvider(int $param) 325 | { 326 | $this->specify('should assert data provider', function ($example) use ($param) { 327 | $this->assertGreaterThanOrEqual(5, $param + $example); 328 | }, ['examples' => [[4], [7], [5]]]); 329 | } 330 | 331 | public function someData(): array 332 | { 333 | return [[1], [2]]; 334 | } 335 | 336 | 337 | 338 | } 339 | 340 | class TestOne 341 | { 342 | /** @specify */ 343 | public $prop; 344 | } 345 | 346 | class User 347 | { 348 | public $name; 349 | } --------------------------------------------------------------------------------