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