最新服务器上的版本,以后用这个
wangzhenxin
2023-11-19 bc164b8bdbfbdf1d8229a5ced6b08d7cb8db7361
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.