├── 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 | [](https://packagist.org/packages/codeception/specify)
7 | [](https://packagist.org/packages/codeception/specify)
8 | [](https://packagist.org/packages/codeception/specify)
9 | [](https://packagist.org/packages/codeception/specify)
10 | [](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 | }
--------------------------------------------------------------------------------