2e86c939
xu
“首次提交”
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
|
# Reusing Test Code
Codeception uses modularity to create a comfortable testing environment for every test suite you write.
Modules allow you to choose the actions and assertions that can be performed in tests.
## What are Actors
All actions and assertions that can be performed by the Actor object in a class are defined in modules.
It might look like Codeception limits you in testing, but that's not true. You can extend the testing suite
with your own actions and assertions, by writing them into a custom module, called a Helper.
We will get back to this later in this chapter, but for now let's look at the following test:
```php
<?php
$I = new AcceptanceTester($scenario);
$I->amOnPage('/');
$I->see('Hello');
$I->seeInDatabase('users', ['id' => 1]);
$I->seeFileFound('running.lock');
```
It can operate with different entities: the web page can be loaded with the PhpBrowser module,
the database assertion uses the Db module, and the file state can be checked with the Filesystem module.
Modules are attached to Actor classes in the suite config.
For example, in `tests/acceptance.suite.yml` we should see:
```yaml
actor: AcceptanceTester
modules:
enabled:
- PhpBrowser:
url: http://localhost
- Db
- Filesystem
```
The AcceptanceTester class has its methods defined in modules.
Let's see what's inside the `AcceptanceTester` class, which is located inside the `tests/_support` directory:
```php
<?php
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method void haveFriend($name, $actorClass = null)
*
* @SuppressWarnings(PHPMD)
*/
class AcceptanceTester extends \Codeception\Actor
{
use _generated\AcceptanceTesterActions;
/**
* Define custom actions here
*/
}
```
The most important part is the `_generated\AcceptanceTesterActions` trait, which is used as a proxy for enabled modules.
It knows which module executes which action and passes parameters into it.
This trait was created by running `codecept build` and is regenerated each time module or configuration changes.
### Authorization
It is recommended to put widely used actions inside an Actor class. A good example is the `login` action
which would probably be actively involved in acceptance or functional testing:
``` php
<?php
class AcceptanceTester extends \Codeception\Actor
{
// do not ever remove this line!
use _generated\AcceptanceTesterActions;
public function login($name, $password)
{
$I = $this;
$I->amOnPage('/login');
$I->submitForm('#loginForm', [
'login' => $name,
'password' => $password
]);
$I->see($name, '.navbar');
}
}
```
Now you can use the `login` method inside your tests:
```php
<?php
$I = new AcceptanceTester($scenario);
$I->login('miles', '123456');
```
However, implementing all actions for reuse in a single actor class may lead to
breaking the [Single Responsibility Principle](http://en.wikipedia.org/wiki/Single_responsibility_principle).
### Session Snapshot
If you need to authorize a user for each test, you can do so by submiting the login form at the beginning of every test.
Running those steps takes time, and in the case of Selenium tests (which are slow by themselves)
that time loss can become significant.
Codeception allows you to share cookies between tests, so a test user can stay logged in for other tests.
Let's improve the code of our `login` method, executing the form submission only once
and restoring the session from cookies for each subsequent login function call:
``` php
<?php
public function login($name, $password)
{
$I = $this;
// if snapshot exists - skipping login
if ($I->loadSessionSnapshot('login')) {
return;
}
// logging in
$I->amOnPage('/login');
$I->submitForm('#loginForm', [
'login' => $name,
'password' => $password
]);
$I->see($name, '.navbar');
// saving snapshot
$I->saveSessionSnapshot('login');
}
```
Note that session restoration only works for `WebDriver` modules
(modules implementing `Codeception\Lib\Interfaces\SessionSnapshot`).
## StepObjects
StepObjects are great if you need some common functionality for a group of tests.
Let's say you are going to test an admin area of a site. You probably won't need the same actions from the admin area
while testing the front end, so it's a good idea to move these admin-specific tests into their own class.
We call such a classes StepObjects.
Lets create an Admin StepObject with the generator:
```bash
php codecept generate:stepobject acceptance Admin
```
You can supply optional action names. Enter one at a time, followed by a newline.
End with an empty line to continue to StepObject creation.
```bash
php codecept generate:stepobject acceptance Admin
Add action to StepObject class (ENTER to exit): loginAsAdmin
Add action to StepObject class (ENTER to exit):
StepObject was created in /tests/acceptance/_support/Step/Acceptance/Admin.php
```
This will generate a class in `/tests/_support/Step/Acceptance/Admin.php` similar to this:
```php
<?php
namespace Step\Acceptance;
class Admin extends \AcceptanceTester
{
public function loginAsAdmin()
{
$I = $this;
}
}
```
As you see, this class is very simple. It extends the `AcceptanceTester` class,
meaning it can access all the methods and properties of `AcceptanceTester`.
The `loginAsAdmin` method may be implemented like this:
```php
<?php
namespace Step\Acceptance;
class Admin extends \AcceptanceTester
{
public function loginAsAdmin()
{
$I = $this;
$I->amOnPage('/admin');
$I->fillField('username', 'admin');
$I->fillField('password', '123456');
$I->click('Login');
}
}
```
In tests, you can use a StepObject by instantiating `Step\Acceptance\Admin` instead of `AcceptanceTester`:
```php
<?php
use Step\Acceptance\Admin as AdminTester;
$I = new AdminTester($scenario);
$I->loginAsAdmin();
```
The same way as above, a StepObject can be instantiated automatically by the Dependency Injection Container
when used inside the Cest format:
```php
<?php
class UserCest
{
function showUserProfile(\Step\Acceptance\Admin $I)
{
$I->loginAsAdmin();
$I->amOnPage('/admin/profile');
$I->see('Admin Profile', 'h1');
}
}
```
If you have a complex interaction scenario, you may use several step objects in one test.
If you feel like adding too many actions into your Actor class
(which is AcceptanceTester in this case) consider moving some of them into separate StepObjects.
## PageObjects
For acceptance and functional testing, we will not only need to have common actions being reused across different tests,
we should have buttons, links and form fields being reused as well. For those cases we need to implement
the [PageObject pattern](http://docs.seleniumhq.org/docs/06_test_design_considerations.jsp#page-object-design-pattern),
which is widely used by test automation engineers. The PageObject pattern represents a web page as a class
and the DOM elements on that page as its properties, and some basic interactions as its methods.
PageObjects are very important when you are developing a flexible architecture of your tests.
Do not hardcode complex CSS or XPath locators in your tests but rather move them into PageObject classes.
Codeception can generate a PageObject class for you with command:
```bash
php codecept generate:pageobject Login
```
This will create a `Login` class in `tests/_support/Page`.
The basic PageObject is nothing more than an empty class with a few stubs.
It is expected that you will populate it with the UI locators of a page it represents
and then those locators will be used on a page.
Locators are represented with public static properties:
```php
<?php
namespace Page;
class Login
{
public static $URL = '/login';
public static $usernameField = '#mainForm #username';
public static $passwordField = '#mainForm input[name=password]';
public static $loginButton = '#mainForm input[type=submit]';
}
```
And this is how this page object can be used in a test:
```php
<?php
use Page\Login as LoginPage;
$I = new AcceptanceTester($scenario);
$I->wantTo('login to site');
$I->amOnPage(LoginPage::$URL);
$I->fillField(LoginPage::$usernameField, 'bill evans');
$I->fillField(LoginPage::$passwordField, 'debby');
$I->click(LoginPage::$loginButton);
$I->see('Welcome, bill');
```
As you see, you can freely change markup of your login page, and all the tests interacting with this page
will have their locators updated according to properties of LoginPage class.
But let's move further. The PageObject concept specifies that the methods for the page interaction
should also be stored in a PageObject class. It now stores a passed instance of an Actor class.
An AcceptanceTester can be accessed via the `AcceptanceTester` property of that class.
Let's define a `login` method in this class:
```php
<?php
namespace Page;
class Login
{
public static $URL = '/login';
public static $usernameField = '#mainForm #username';
public static $passwordField = '#mainForm input[name=password]';
public static $loginButton = '#mainForm input[type=submit]';
/**
* @var AcceptanceTester
*/
protected $tester;
public function __construct(\AcceptanceTester $I)
{
$this->tester = $I;
}
public function login($name, $password)
{
$I = $this->tester;
$I->amOnPage(self::$URL);
$I->fillField(self::$usernameField, $name);
$I->fillField(self::$passwordField, $password);
$I->click(self::$loginButton);
return $this;
}
}
```
And here is an example of how this PageObject can be used in a test:
```php
<?php
use Page\Login as LoginPage;
$I = new AcceptanceTester($scenario);
$loginPage = new LoginPage($I);
$loginPage->login('bill evans', 'debby');
$I->amOnPage('/profile');
$I->see('Bill Evans Profile', 'h1');
```
If you write your scenario-driven tests in the Cest format (which is the recommended approach),
you can bypass the manual creation of a PageObject and delegate this task to Codeception.
If you specify which object you need for a test, Codeception will try to create it using the dependency injection container.
In the case of a PageObject you should declare a class as a parameter for a test method:
```php
<?php
class UserCest
{
function showUserProfile(AcceptanceTester $I, \Page\Login $loginPage)
{
$loginPage->login('bill evans', 'debby');
$I->amOnPage('/profile');
$I->see('Bill Evans Profile', 'h1');
}
}
```
The dependency injection container can construct any object that requires any known class type.
For instance, `Page\Login` required `AcceptanceTester`, and so it was injected into `Page\Login` constructor,
and PageObject was created and passed into method arguments. You should explicitly specify
the types of required objects for Codeception to know what objects should be created for a test.
Dependency Injection will be described in the next chapter.
## Conclusion
There are lots of ways to create reusable and readable tests. Group common actions together
and move them to an Actor class or StepObjects. Move CSS and XPath locators into PageObjects.
Write your custom actions and assertions in Helpers.
Scenario-driven tests should not contain anything more complex than `$I->doSomething` commands.
Following this approach will allow you to keep your tests clean, readable, stable and make them easy to maintain.
|