commit | author | age
|
2207d6
|
1 |
# Unit & Integration Tests |
W |
2 |
|
|
3 |
Codeception uses PHPUnit as a backend for running its tests. Thus, any PHPUnit test can be added to a Codeception test suite |
|
4 |
and then executed. If you ever wrote a PHPUnit test then do it just as you did before. |
|
5 |
Codeception adds some nice helpers to simplify common tasks. |
|
6 |
|
|
7 |
## Creating a Test |
|
8 |
|
|
9 |
Create a test using `generate:test` command with a suite and test names as parameters: |
|
10 |
|
|
11 |
```bash |
|
12 |
php vendor/bin/codecept generate:test unit Example |
|
13 |
``` |
|
14 |
|
|
15 |
It creates a new `ExampleTest` file located in the `tests/unit` directory. |
|
16 |
|
|
17 |
As always, you can run the newly created test with this command: |
|
18 |
|
|
19 |
```bash |
|
20 |
php vendor/bin/codecept run unit ExampleTest |
|
21 |
``` |
|
22 |
|
|
23 |
Or simply run the whole set of unit tests with: |
|
24 |
|
|
25 |
```bash |
|
26 |
php vendor/bin/codecept run unit |
|
27 |
``` |
|
28 |
|
|
29 |
A test created by the `generate:test` command will look like this: |
|
30 |
|
|
31 |
```php |
|
32 |
<?php |
|
33 |
|
|
34 |
class ExampleTest extends \Codeception\Test\Unit |
|
35 |
{ |
|
36 |
/** |
|
37 |
* @var \UnitTester |
|
38 |
*/ |
|
39 |
protected $tester; |
|
40 |
|
|
41 |
protected function _before() |
|
42 |
{ |
|
43 |
} |
|
44 |
|
|
45 |
protected function _after() |
|
46 |
{ |
|
47 |
} |
|
48 |
|
|
49 |
// tests |
|
50 |
public function testMe() |
|
51 |
{ |
|
52 |
|
|
53 |
} |
|
54 |
} |
|
55 |
``` |
|
56 |
|
|
57 |
Inside a class: |
|
58 |
|
|
59 |
* all public methods with `test` prefix are tests |
|
60 |
* `_before` method is executed before each test (like `setUp` in PHPUnit) |
|
61 |
* `_after` method is executed after each test (like `tearDown` in PHPUnit) |
|
62 |
|
|
63 |
## Unit Testing |
|
64 |
|
|
65 |
Unit tests are focused around a single component of an application. |
|
66 |
All external dependencies for components should be replaced with test doubles. |
|
67 |
|
|
68 |
A typical unit test may look like this: |
|
69 |
|
|
70 |
```php |
|
71 |
<?php |
|
72 |
class UserTest extends \Codeception\Test\Unit |
|
73 |
{ |
|
74 |
public function testValidation() |
|
75 |
{ |
|
76 |
$user = new User(); |
|
77 |
|
|
78 |
$user->setName(null); |
|
79 |
$this->assertFalse($user->validate(['username'])); |
|
80 |
|
|
81 |
$user->setName('toolooooongnaaaaaaameeee'); |
|
82 |
$this->assertFalse($user->validate(['username'])); |
|
83 |
|
|
84 |
$user->setName('davert'); |
|
85 |
$this->assertTrue($user->validate(['username'])); |
|
86 |
} |
|
87 |
} |
|
88 |
``` |
|
89 |
|
|
90 |
### Assertions |
|
91 |
|
|
92 |
There are pretty many assertions you can use inside tests. The most common are: |
|
93 |
|
|
94 |
* `$this->assertEquals()` |
|
95 |
* `$this->assertContains()` |
|
96 |
* `$this->assertFalse()` |
|
97 |
* `$this->assertTrue()` |
|
98 |
* `$this->assertNull()` |
|
99 |
* `$this->assertEmpty()` |
|
100 |
|
|
101 |
Assertion methods come from PHPUnit. [See the complete reference at phpunit.de](https://phpunit.de/manual/current/en/appendixes.assertions.html). |
|
102 |
|
|
103 |
### Test Doubles |
|
104 |
|
|
105 |
Codeception provides [Codeception\Stub library](https://github.com/Codeception/Stub) for building mocks and stubs for tests. |
|
106 |
Under the hood it used PHPUnit's mock builder but with much simplified API. |
|
107 |
|
|
108 |
Alternatively, [Mockery](https://github.com/Codeception/MockeryModule) can be used inside Codeception. |
|
109 |
|
|
110 |
#### Stubs |
|
111 |
|
|
112 |
Stubs can be created with a static methods of `Codeception\Stub`. |
|
113 |
|
|
114 |
```php |
|
115 |
<?php |
|
116 |
$user = \Codeception\Stub::make('User', ['getName' => 'john']); |
|
117 |
$name = $user->getName(); // 'john' |
|
118 |
``` |
|
119 |
|
|
120 |
[See complete reference](http://codeception.com/docs/reference/Mock) |
|
121 |
|
|
122 |
Inside unit tests (`Codeception\Test\Unit`) it is recommended to use alternative API: |
|
123 |
|
|
124 |
```php |
|
125 |
<?php |
|
126 |
// create a stub with find method replaced |
|
127 |
$userRepository = $this->make(UserRepository::class, ['find' => new User]); |
|
128 |
$userRepository->find(1); // => User |
|
129 |
|
|
130 |
// create a dummy |
|
131 |
$userRepository = $this->makeEmpty(UserRepository::class); |
|
132 |
|
|
133 |
// create a stub with all methods replaced except one |
|
134 |
$user = $this->makeEmptyExcept(User::class, 'validate'); |
|
135 |
$user->validate($data); |
|
136 |
|
|
137 |
// create a stub by calling constructor and replacing a method |
|
138 |
$user = $this->construct(User::class, ['name' => 'davert'], ['save' => false]); |
|
139 |
|
|
140 |
// create a stub by calling constructor with empty methods |
|
141 |
$user = $this->constructEmpty(User::class, ['name' => 'davert']); |
|
142 |
|
|
143 |
// create a stub by calling constructor with empty methods |
|
144 |
$user = $this->constructEmptyExcept(User::class, 'getName', ['name' => 'davert']); |
|
145 |
$user->getName(); // => davert |
|
146 |
$user->setName('jane'); // => this method is empty |
|
147 |
``` |
|
148 |
|
|
149 |
[See complete reference](http://codeception.com/docs/reference/Mock) |
|
150 |
|
|
151 |
Stubs can also be created using static methods from `Codeception\Stub` class. |
|
152 |
In this |
|
153 |
|
|
154 |
```php |
|
155 |
<?php |
|
156 |
\Codeception\Stub::make(UserRepository::class, ['find' => new User]); |
|
157 |
``` |
|
158 |
|
|
159 |
See a reference for [static Stub API](http://codeception.com/docs/reference/Stub) |
|
160 |
|
|
161 |
#### Mocks |
|
162 |
|
|
163 |
To declare expectations for mocks use `Codeception\Stub\Expected` class: |
|
164 |
|
|
165 |
```php |
|
166 |
<?php |
|
167 |
// create a mock where $user->getName() should never be called |
|
168 |
$user = $this->make('User', [ |
|
169 |
'getName' => Expected::never(), |
|
170 |
'someMethod' => function() {} |
|
171 |
]); |
|
172 |
$user->someMethod(); |
|
173 |
|
|
174 |
// create a mock where $user->getName() should be called at least once |
|
175 |
$user = $this->make('User', [ |
|
176 |
'getName' => Expected::atLeastOnce('Davert') |
|
177 |
] |
|
178 |
); |
|
179 |
$user->getName(); |
|
180 |
$userName = $user->getName(); |
|
181 |
$this->assertEquals('Davert', $userName); |
|
182 |
``` |
|
183 |
|
|
184 |
[See complete reference](http://codeception.com/docs/reference/Mock) |
|
185 |
|
|
186 |
## Integration Tests |
|
187 |
|
|
188 |
Unlike unit tests integration tests doesn't require the code to be executed in isolation. |
|
189 |
That allows us to use database and other components inside a tests. |
|
190 |
To improve the testing experience modules can be used as in functional testing. |
|
191 |
|
|
192 |
### Using Modules |
|
193 |
|
|
194 |
As in scenario-driven functional or acceptance tests you can access Actor class methods. |
|
195 |
If you write integration tests, it may be useful to include the `Db` module for database testing. |
|
196 |
|
|
197 |
```yaml |
|
198 |
# Codeception Test Suite Configuration |
|
199 |
|
|
200 |
# suite for unit (internal) tests. |
|
201 |
actor: UnitTester |
|
202 |
modules: |
|
203 |
enabled: |
|
204 |
- Asserts |
|
205 |
- Db |
|
206 |
- \Helper\Unit |
|
207 |
``` |
|
208 |
|
|
209 |
To access UnitTester methods you can use the `UnitTester` property in a test. |
|
210 |
|
|
211 |
### Testing Database |
|
212 |
|
|
213 |
Let's see how you can do some database testing: |
|
214 |
|
|
215 |
```php |
|
216 |
<?php |
|
217 |
function testSavingUser() |
|
218 |
{ |
|
219 |
$user = new User(); |
|
220 |
$user->setName('Miles'); |
|
221 |
$user->setSurname('Davis'); |
|
222 |
$user->save(); |
|
223 |
$this->assertEquals('Miles Davis', $user->getFullName()); |
|
224 |
$this->tester->seeInDatabase('users', ['name' => 'Miles', 'surname' => 'Davis']); |
|
225 |
} |
|
226 |
``` |
|
227 |
|
|
228 |
To enable the database functionality in unit tests, make sure the `Db` module is included |
|
229 |
in the `unit.suite.yml` configuration file. |
|
230 |
The database will be cleaned and populated after each test, the same way it happens for acceptance and functional tests. |
|
231 |
If that's not your required behavior, change the settings of the `Db` module for the current suite. See [Db Module](http://codeception.com/docs/modules/Db) |
|
232 |
|
|
233 |
### Interacting with the Framework |
|
234 |
|
|
235 |
You should probably not access your database directly if your project already uses ORM for database interactions. |
|
236 |
Why not use ORM directly inside your tests? Let's try to write a test using Laravel's ORM Eloquent. |
|
237 |
For this we need to configure the Laravel5 module. We won't need its web interaction methods like `amOnPage` or `see`, |
|
238 |
so let's enable only the ORM part of it: |
|
239 |
|
|
240 |
```yaml |
|
241 |
actor: UnitTester |
|
242 |
modules: |
|
243 |
enabled: |
|
244 |
- Asserts |
|
245 |
- Laravel5: |
|
246 |
part: ORM |
|
247 |
- \Helper\Unit |
|
248 |
``` |
|
249 |
|
|
250 |
We included the Laravel5 module the same way we did for functional testing. |
|
251 |
Let's see how we can use it for integration tests: |
|
252 |
|
|
253 |
```php |
|
254 |
<?php |
|
255 |
function testUserNameCanBeChanged() |
|
256 |
{ |
|
257 |
// create a user from framework, user will be deleted after the test |
|
258 |
$id = $this->tester->haveRecord('users', ['name' => 'miles']); |
|
259 |
// access model |
|
260 |
$user = User::find($id); |
|
261 |
$user->setName('bill'); |
|
262 |
$user->save(); |
|
263 |
$this->assertEquals('bill', $user->getName()); |
|
264 |
// verify data was saved using framework methods |
|
265 |
$this->tester->seeRecord('users', ['name' => 'bill']); |
|
266 |
$this->tester->dontSeeRecord('users', ['name' => 'miles']); |
|
267 |
} |
|
268 |
``` |
|
269 |
|
|
270 |
A very similar approach can be used for all frameworks that have an ORM implementing the ActiveRecord pattern. |
|
271 |
In Yii2 and Phalcon, the methods `haveRecord`, `seeRecord`, `dontSeeRecord` work in the same way. |
|
272 |
They also should be included by specifying `part: ORM` in order to not use the functional testing actions. |
|
273 |
|
|
274 |
If you are using Symfony with Doctrine, you don't need to enable Symfony itself but just Doctrine2: |
|
275 |
|
|
276 |
```yaml |
|
277 |
actor: UnitTester |
|
278 |
modules: |
|
279 |
enabled: |
|
280 |
- Asserts |
|
281 |
- Doctrine2: |
|
282 |
depends: Symfony |
|
283 |
- \Helper\Unit |
|
284 |
``` |
|
285 |
|
|
286 |
In this case you can use the methods from the Doctrine2 module, while Doctrine itself uses the Symfony module |
|
287 |
to establish connections to the database. In this case a test might look like: |
|
288 |
|
|
289 |
```php |
|
290 |
<?php |
|
291 |
function testUserNameCanBeChanged() |
|
292 |
{ |
|
293 |
// create a user from framework, user will be deleted after the test |
|
294 |
$id = $this->tester->haveInRepository(User::class, ['name' => 'miles']); |
|
295 |
// get entity manager by accessing module |
|
296 |
$em = $this->getModule('Doctrine2')->em; |
|
297 |
// get real user |
|
298 |
$user = $em->find(User::class, $id); |
|
299 |
$user->setName('bill'); |
|
300 |
$em->persist($user); |
|
301 |
$em->flush(); |
|
302 |
$this->assertEquals('bill', $user->getName()); |
|
303 |
// verify data was saved using framework methods |
|
304 |
$this->tester->seeInRepository(User::class, ['name' => 'bill']); |
|
305 |
$this->tester->dontSeeInRepository(User::class, ['name' => 'miles']); |
|
306 |
} |
|
307 |
``` |
|
308 |
|
|
309 |
In both examples you should not be worried about the data persistence between tests. |
|
310 |
The Doctrine2 and Laravel5 modules will clean up the created data at the end of a test. |
|
311 |
This is done by wrapping each test in a transaction and rolling it back afterwards. |
|
312 |
|
|
313 |
### Accessing Module |
|
314 |
|
|
315 |
Codeception allows you to access the properties and methods of all modules defined for this suite. |
|
316 |
Unlike using the UnitTester class for this purpose, using a module directly grants you access |
|
317 |
to all public properties of that module. |
|
318 |
|
|
319 |
We have already demonstrated this in a previous example where we accessed the Entity Manager from a Doctrine2 module: |
|
320 |
|
|
321 |
```php |
|
322 |
<?php |
|
323 |
/** @var Doctrine\ORM\EntityManager */ |
|
324 |
$em = $this->getModule('Doctrine2')->em; |
|
325 |
``` |
|
326 |
|
|
327 |
If you use the `Symfony` module, here is how you can access the Symfony container: |
|
328 |
|
|
329 |
```php |
|
330 |
<?php |
|
331 |
/** @var Symfony\Component\DependencyInjection\Container */ |
|
332 |
$container = $this->getModule('Symfony')->container; |
|
333 |
``` |
|
334 |
|
|
335 |
The same can be done for all public properties of an enabled module. Accessible properties are listed in the module reference. |
|
336 |
|
|
337 |
### Scenario Driven Testing |
|
338 |
|
|
339 |
[Cest format](http://codeception.com/docs/07-AdvancedUsage#Cest-Classes) can also be used for integration testing. |
|
340 |
In some cases it makes tests cleaner as it simplifies module access by using common `$I->` syntax: |
|
341 |
|
|
342 |
```php |
|
343 |
<?php |
|
344 |
public function buildShouldHaveSequence(\UnitTester $I) |
|
345 |
{ |
|
346 |
$build = $I->have(Build::class, ['project_id' => $this->project->id]); |
|
347 |
$I->assertEquals(1, $build->sequence); |
|
348 |
$build = $I->have(Build::class, ['project_id' => $this->project->id]); |
|
349 |
$I->assertEquals(2, $build->sequence); |
|
350 |
$this->project->refresh(); |
|
351 |
$I->assertEquals(3, $this->project->build_sequence); |
|
352 |
} |
|
353 |
``` |
|
354 |
This format can be recommended for testing domain and database interactions. |
|
355 |
|
|
356 |
In Cest format you don't have native support for test doubles so it's recommended |
|
357 |
to include a trait `\Codeception\Test\Feature\Stub` to enable mocks inside a test. |
|
358 |
Alternatively, install and enable [Mockery module](https://github.com/Codeception/MockeryModule). |
|
359 |
|
|
360 |
## Advanced Tools |
|
361 |
|
|
362 |
### Specify |
|
363 |
|
|
364 |
When writing tests you should prepare them for constant changes in your application. |
|
365 |
Tests should be easy to read and maintain. If a specification of your application is changed, |
|
366 |
your tests should be updated as well. If you don't have a convention inside your team for documenting tests, |
|
367 |
you will have issues figuring out what tests will be affected by the introduction of a new feature. |
|
368 |
|
|
369 |
That's why it's pretty important not just to cover your application with unit tests, but make unit tests self-explanatory. |
|
370 |
We do this for scenario-driven acceptance and functional tests, and we should do this for unit and integration tests as well. |
|
371 |
|
|
372 |
For this case we have a stand-alone project [Specify](https://github.com/Codeception/Specify) |
|
373 |
(which is included in the phar package) for writing specifications inside unit tests: |
|
374 |
|
|
375 |
```php |
|
376 |
<?php |
|
377 |
class UserTest extends \Codeception\Test\Unit |
|
378 |
{ |
|
379 |
use \Codeception\Specify; |
|
380 |
|
|
381 |
/** @specify */ |
|
382 |
private $user; |
|
383 |
|
|
384 |
public function testValidation() |
|
385 |
{ |
|
386 |
$this->user = User::create(); |
|
387 |
|
|
388 |
$this->specify("username is required", function() { |
|
389 |
$this->user->username = null; |
|
390 |
$this->assertFalse($this->user->validate(['username'])); |
|
391 |
}); |
|
392 |
|
|
393 |
$this->specify("username is too long", function() { |
|
394 |
$this->user->username = 'toolooooongnaaaaaaameeee'; |
|
395 |
$this->assertFalse($this->user->validate(['username'])); |
|
396 |
}); |
|
397 |
|
|
398 |
$this->specify("username is ok", function() { |
|
399 |
$this->user->username = 'davert'; |
|
400 |
$this->assertTrue($this->user->validate(['username'])); |
|
401 |
}); |
|
402 |
} |
|
403 |
} |
|
404 |
``` |
|
405 |
|
|
406 |
By using `specify` codeblocks, you can describe any piece of a test. |
|
407 |
This makes tests much cleaner and comprehensible for everyone in your team. |
|
408 |
|
|
409 |
Code inside `specify` blocks is isolated. In the example above, any changes to `$this->user` |
|
410 |
will not be reflected in other code blocks as it is marked with `@specify` annotation. |
|
411 |
|
|
412 |
Also, you may add [Codeception\Verify](https://github.com/Codeception/Verify) for BDD-style assertions. |
|
413 |
This tiny library adds more readable assertions, which is quite nice, if you are always confused |
|
414 |
about which argument in `assert` calls is expected and which one is actual: |
|
415 |
|
|
416 |
```php |
|
417 |
<?php |
|
418 |
verify($user->getName())->equals('john'); |
|
419 |
``` |
|
420 |
|
|
421 |
### Domain Assertions |
|
422 |
|
|
423 |
The more complicated your domain is the more explicit your tests should be. With [DomainAssert](https://github.com/Codeception/DomainAssert) |
|
424 |
library you can easily create custom assertion methods for unit and integration tests. |
|
425 |
|
|
426 |
It allows to reuse business rules inside assertion methods: |
|
427 |
|
|
428 |
```php |
|
429 |
<?php |
|
430 |
$user = new User; |
|
431 |
|
|
432 |
// simple custom assertions below: |
|
433 |
$this->assertUserIsValid($user); |
|
434 |
$this->assertUserIsAdmin($user); |
|
435 |
|
|
436 |
// use combined explicit assertion |
|
437 |
// to tell what you expect to check |
|
438 |
$this->assertUserCanPostToBlog($user, $blog); |
|
439 |
// instead of just calling a bunch of assertions |
|
440 |
$this->assertNotNull($user); |
|
441 |
$this->assertNotNull($blog); |
|
442 |
$this->assertContain($user, $blog->getOwners()); |
|
443 |
``` |
|
444 |
|
|
445 |
With custom assertion methods you can improve readability of your tests and keep them focused around the specification. |
|
446 |
|
|
447 |
### AspectMock |
|
448 |
|
|
449 |
[AspectMock](https://github.com/Codeception/AspectMock) is an advanced mocking framework which allows you to replace any methods of any class in a test. |
|
450 |
Static methods, class methods, date and time functions can be easily replaced with AspectMock. |
|
451 |
For instance, you can test singletons! |
|
452 |
|
|
453 |
```php |
|
454 |
<?php |
|
455 |
public function testSingleton() |
|
456 |
{ |
|
457 |
$class = MySingleton::getInstance(); |
|
458 |
$this->assertInstanceOf('MySingleton', $class); |
|
459 |
test::double('MySingleton', ['getInstance' => new DOMDocument]); |
|
460 |
$this->assertInstanceOf('DOMDocument', $class); |
|
461 |
} |
|
462 |
``` |
|
463 |
|
|
464 |
* [AspectMock on GitHub](https://github.com/Codeception/AspectMock) |
|
465 |
* [AspectMock in Action](http://codeception.com/07-31-2013/nothing-is-untestable-aspect-mock.html) |
|
466 |
* [How it Works](http://codeception.com/09-13-2013/understanding-aspectmock.html) |
|
467 |
|
|
468 |
## Conclusion |
|
469 |
|
|
470 |
PHPUnit tests are first-class citizens in test suites. Whenever you need to write and execute unit tests, |
|
471 |
you don't need to install PHPUnit separately, but use Codeception directly to execute them. |
|
472 |
Some nice features can be added to common unit tests by integrating Codeception modules. |
|
473 |
For most unit and integration testing, PHPUnit tests are enough. They run fast, and are easy to maintain. |