|
3 | 3 | @name E2E Testing |
4 | 4 | @description |
5 | 5 |
|
| 6 | +# E2E Testing |
| 7 | + |
6 | 8 | <div class="alert alert-danger"> |
7 | | -**Note:** Angular Scenario Runner is depricated. If you're starting a new Angular project, |
8 | | -consider using [Protractor](https://github.com/angular/protractor). |
| 9 | +**Note:** In the past, end to end testing could be done with a deprecated tool called |
| 10 | +[Angular Scenario Runner](http://code.angularjs.org/1.2.16/docs/guide/e2e-testing). That tool |
| 11 | +is now in maintenance mode. |
9 | 12 | </div> |
10 | 13 |
|
11 | | -# E2E Testing with the Angular Scenario Runner |
12 | | - |
13 | 14 | As applications grow in size and complexity, it becomes unrealistic to rely on manual testing to |
14 | | -verify the correctness of new features, catch bugs and notice regressions. |
| 15 | +verify the correctness of new features, catch bugs and notice regressions. End to end tests |
| 16 | +are the first line of defense for catching bugs, but sometimes issues come up with integration |
| 17 | +between components which can't be captured in a unit test. End to end tests are made to find |
| 18 | +these problems. |
| 19 | + |
| 20 | +We have built [Protractor](https://github.com/angular/protractor), an end |
| 21 | +to end test runner which simulates user interactions that will help you verify the health of your |
| 22 | +Angular application. |
15 | 23 |
|
16 | | -To solve this problem, we have built an Angular Scenario Runner which simulates user interactions |
17 | | -that will help you verify the health of your Angular application. |
| 24 | +## Using Protractor |
18 | 25 |
|
19 | | -## Overview |
| 26 | +Protractor is a [Node.js](http://nodejs.org) program, and runs end to end tests that are also |
| 27 | +written in JavaScript and run with node. Protractor uses [WebDriver](https://code.google.com/p/selenium/wiki/GettingStarted) |
| 28 | +to control browsers and simulate user actions. |
20 | 29 |
|
21 | | -You write scenario tests in JavaScript. These tests describe how your application should behave |
22 | | -given a certain interaction in a specific state. |
| 30 | +For more information on Protractor, view [getting started](https://github.com/angular/protractor/blob/master/docs/getting-started.md) |
| 31 | +or the [api docs](https://github.com/angular/protractor/blob/master/docs/api.md). |
23 | 32 |
|
24 | | -A scenario is comprised of one or more `it` blocks that describe the requirements of your |
25 | | -application. `it` blocks are made of **commands** and **expectations**. Commands tell the Runner |
26 | | -to do something with the application such as navigate to a page or click on a button. Expectations |
27 | | -tell the Runner to assert something about the application's state, such as the value of a field or |
28 | | -the current URL. |
| 33 | +Protractor uses [Jasmine](http://jasmine.github.io/1.3/introduction.html) for its test syntax. |
| 34 | +As in unit testing, a test file is comprised of one or |
| 35 | +more `it` blocks that describe the requirements of your application. `it` blocks are made of |
| 36 | +**commands** and **expectations**. Commands tell Protractor to do something with the application |
| 37 | +such as navigate to a page or click on a button. Expectations tell Protractor to assert something |
| 38 | +about the application's state, such as the value of a field or the current URL. |
29 | 39 |
|
30 | 40 | If any expectation within an `it` block fails, the runner marks the `it` as "failed" and continues |
31 | 41 | on to the next block. |
32 | 42 |
|
33 | | -Scenarios may also have `beforeEach` and `afterEach` blocks, which will be run before or after |
| 43 | +Test files may also have `beforeEach` and `afterEach` blocks, which will be run before or after |
34 | 44 | each `it` block regardless of whether the block passes or fails. |
35 | 45 |
|
36 | 46 | <img src="img/guide/scenario_runner.png"> |
37 | 47 |
|
38 | | -In addition to the above elements, scenarios may also contain helper functions to avoid duplicating |
| 48 | +In addition to the above elements, tests may also contain helper functions to avoid duplicating |
39 | 49 | code in the `it` blocks. |
40 | 50 |
|
41 | | -Here is an example of a simple scenario: |
| 51 | +Here is an example of a simple test: |
42 | 52 | ```js |
43 | | -describe('Buzz Client', function() { |
44 | | -it('should filter results', function() { |
45 | | - input('user').enter('jacksparrow'); |
46 | | - element(':button').click(); |
47 | | - expect(repeater('ul li').count()).toEqual(10); |
48 | | - input('filterText').enter('Bees'); |
49 | | - expect(repeater('ul li').count()).toEqual(1); |
50 | | -}); |
51 | | -}); |
52 | | -``` |
53 | | - |
54 | | -Note that |
55 | | -[`input('user')`](https://github.com/angular/angular.js/blob/master/docs/content/guide/dev_guide.e2e-testing.ngdoc#L119) |
56 | | -finds the `<input>` element with `ng-model="user"` not `name="user"`. |
57 | | - |
58 | | -This scenario describes the requirements of a Buzz Client, specifically, that it should be able to |
59 | | -filter the stream of the user. It starts by entering a value in the input field with ng-model="user", clicking |
60 | | -the only button on the page, and then it verifies that there are 10 items listed. It then enters |
61 | | -'Bees' in the input field with ng-model='filterText' and verifies that the list is reduced to a single item. |
62 | | - |
63 | | -The API section below lists the available commands and expectations for the Runner. |
64 | | - |
65 | | -## API |
66 | | -Source: https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js |
67 | | - |
68 | | -### `pause()` |
69 | | -Pauses the execution of the tests until you call `resume()` in the console (or click the resume |
70 | | -link in the Runner UI). |
71 | | - |
72 | | -### `sleep(seconds)` |
73 | | -Pauses the execution of the tests for the specified number of `seconds`. |
74 | | - |
75 | | -### `browser().navigateTo(url)` |
76 | | -Loads the `url` into the test frame. |
77 | | - |
78 | | -### `browser().navigateTo(url, fn)` |
79 | | -Loads the URL returned by `fn` into the testing frame. The given `url` is only used for the test |
80 | | -output. Use this when the destination URL is dynamic (that is, the destination is unknown when you |
81 | | -write the test). |
82 | | - |
83 | | -### `browser().reload()` |
84 | | -Refreshes the currently loaded page in the test frame. |
85 | | - |
86 | | -### `browser().window().href()` |
87 | | -Returns the window.location.href of the currently loaded page in the test frame. |
88 | | - |
89 | | -### `browser().window().path()` |
90 | | -Returns the window.location.pathname of the currently loaded page in the test frame. |
91 | | - |
92 | | -### `browser().window().search()` |
93 | | -Returns the window.location.search of the currently loaded page in the test frame. |
94 | | - |
95 | | -### `browser().window().hash()` |
96 | | -Returns the window.location.hash (without `#`) of the currently loaded page in the test frame. |
97 | | - |
98 | | -### `browser().location().url()` |
99 | | -Returns the {@link ng.$location $location.url()} of the currently loaded page in |
100 | | -the test frame. |
101 | | - |
102 | | -### `browser().location().path()` |
103 | | -Returns the {@link ng.$location $location.path()} of the currently loaded page in |
104 | | -the test frame. |
105 | | - |
106 | | -### `browser().location().search()` |
107 | | -Returns the {@link ng.$location $location.search()} of the currently loaded page |
108 | | -in the test frame. |
109 | | - |
110 | | -### `browser().location().hash()` |
111 | | -Returns the {@link ng.$location $location.hash()} of the currently loaded page in |
112 | | -the test frame. |
113 | | - |
114 | | -### `expect(future).{matcher}` |
115 | | -Asserts the value of the given `future` satisfies the `matcher`. All API statements return a |
116 | | -`future` object, which get a `value` assigned after they are executed. Matchers are defined using |
117 | | -`angular.scenario.matcher`, and they use the value of futures to run the expectation. For example: |
118 | | -`expect(browser().location().href()).toEqual('http://www.google.com')`. Available matchers |
119 | | -are presented further down this document. |
120 | | - |
121 | | -### `expect(future).not().{matcher}` |
122 | | -Asserts the value of the given `future` satisfies the negation of the `matcher`. |
123 | | - |
124 | | -### `using(selector, label)` |
125 | | -Scopes the next DSL element selection. |
126 | | - |
127 | | -### `binding(name)` |
128 | | -Returns the value of the first binding matching the given `name`. |
129 | | - |
130 | | -### `input(name).enter(value)` |
131 | | -Enters the given `value` in the text field with the corresponding ng-model `name`. |
132 | | - |
133 | | -### `input(name).check()` |
134 | | -Checks/unchecks the checkbox with the corresponding ng-model `name`. |
135 | | - |
136 | | -### `input(name).select(value)` |
137 | | -Selects the given `value` in the radio button with the corresponding ng-model `name`. |
138 | | - |
139 | | -### `input(name).val()` |
140 | | -Returns the current value of an input field with the corresponding ng-model `name`. |
141 | | - |
142 | | -### `repeater(selector, label).count()` |
143 | | -Returns the number of rows in the repeater matching the given jQuery `selector`. The `label` is |
144 | | -used for test output. |
145 | | - |
146 | | -### `repeater(selector, label).row(index)` |
147 | | -Returns an array with the bindings in the row at the given `index` in the repeater matching the |
148 | | -given jQuery `selector`. The `label` is used for test output. |
149 | | - |
150 | | -### `repeater(selector, label).column(binding)` |
151 | | -Returns an array with the values in the column with the given `binding` in the repeater matching |
152 | | -the given jQuery `selector`. The `label` is used for test output. |
153 | | - |
154 | | -### `select(name).option(value)` |
155 | | -Picks the option with the given `value` on the select with the given ng-model `name`. |
156 | | - |
157 | | -### `select(name).options(value1, value2...)` |
158 | | -Picks the options with the given `values` on the multi select with the given ng-model `name`. |
159 | | - |
160 | | -### `element(selector, label).count()` |
161 | | -Returns the number of elements that match the given jQuery `selector`. The `label` is used for test |
162 | | -output. |
| 53 | +describe('TODO list', function() { |
| 54 | + it('should filter results', function() { |
163 | 55 |
|
164 | | -### `element(selector, label).click()` |
165 | | -Clicks on the element matching the given jQuery `selector`. The `label` is used for test output. |
| 56 | + // Find the element with ng-model="user" and type "jacksparrow" into it |
| 57 | + element(by.model('user')).sendKeys('jacksparrow'); |
166 | 58 |
|
167 | | -### `element(selector, label).query(fn)` |
168 | | -Executes the function `fn(selectedElements, done)`, where selectedElements are the elements that |
169 | | -match the given jQuery `selector` and `done` is a function that is called at the end of the `fn` |
170 | | -function. The `label` is used for test output. |
| 59 | + // Find the first (and only) button on the page and click it |
| 60 | + element(by.css(':button')).click(); |
171 | 61 |
|
172 | | -### `element(selector, label).{method}()` |
173 | | -Returns the result of calling `method` on the element matching the given jQuery `selector`, where |
174 | | -`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`, |
175 | | -`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`, |
176 | | -`scrollTop`, `offset`. The `label` is used for test output. |
| 62 | + // Verify that there are 10 tasks |
| 63 | + expect(element.all(by.repeater('task in tasks')).count()).toEqual(10); |
177 | 64 |
|
178 | | -### `element(selector, label).{method}(value)` |
179 | | -Executes the `method` passing in `value` on the element matching the given jQuery `selector`, where |
180 | | -`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`, |
181 | | -`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`, |
182 | | -`scrollTop`, `offset`. The `label` is used for test output. |
| 65 | + // Enter 'groceries' into the element with ng-model="filterText" |
| 66 | + element(by.model('filterText')).sendKeys('groceries'); |
183 | 67 |
|
184 | | -### `element(selector, label).{method}(key)` |
185 | | -Returns the result of calling `method` passing in `key` on the element matching the given jQuery |
186 | | -`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The |
187 | | -`label` is used for test output. |
188 | | - |
189 | | -### `element(selector, label).{method}(key, value)` |
190 | | -Executes the `method` passing in `key` and `value` on the element matching the given jQuery |
191 | | -`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The |
192 | | -`label` is used for test output. |
193 | | - |
194 | | -## Matchers |
195 | | - |
196 | | -Matchers are used in combination with the `expect(...)` function as described above and can |
197 | | -be negated with `not()`. For instance: `expect(element('h1').text()).not().toEqual('Error')`. |
198 | | - |
199 | | -Source: https://github.com/angular/angular.js/blob/master/src/ngScenario/matchers.js |
200 | | - |
201 | | -```js |
202 | | -// value and Object comparison following the rules of angular.equals(). |
203 | | -expect(value).toEqual(value) |
204 | | - |
205 | | -// a simpler value comparison using === |
206 | | -expect(value).toBe(value) |
207 | | - |
208 | | -// checks that the value is defined by checking its type. |
209 | | -expect(value).toBeDefined() |
210 | | - |
211 | | -// the following two matchers are using JavaScript's standard truthiness rules |
212 | | -expect(value).toBeTruthy() |
213 | | -expect(value).toBeFalsy() |
214 | | - |
215 | | -// verify that the value matches the given regular expression. The regular |
216 | | -// expression may be passed in form of a string or a regular expression |
217 | | -// object. |
218 | | -expect(value).toMatch(expectedRegExp) |
219 | | - |
220 | | -// a check for null using === |
221 | | -expect(value).toBeNull() |
222 | | - |
223 | | -// Array.indexOf(...) is used internally to check whether the element is |
224 | | -// contained within the array. |
225 | | -expect(value).toContain(expected) |
226 | | - |
227 | | -// number comparison using < and > |
228 | | -expect(value).toBeLessThan(expected) |
229 | | -expect(value).toBeGreaterThan(expected) |
230 | | -``` |
231 | | - |
232 | | -## Example |
233 | | -See the [angular-seed](https://github.com/angular/angular-seed) project for more examples. |
234 | | - |
235 | | -### Conditional actions with element(...).query(fn) |
236 | | - |
237 | | -E2E testing with angular scenario is highly asynchronous and hides a lot of complexity by |
238 | | -queueing actions and expectations that can handle futures. From time to time, you might need |
239 | | -conditional assertions or element selection. Even though you should generally try to avoid this |
240 | | -(as it is can be sign for unstable tests), you can add conditional behavior with |
241 | | -`element(...).query(fn)`. The following code listing shows how this function can be used to delete |
242 | | -added entries (where an entry is some domain object) using the application's web interface. |
243 | | - |
244 | | -Imagine the application to be structured into two views: |
245 | | - |
246 | | - 1. *Overview view* which lists all the added entries in a table and |
247 | | - 2. a *detail view* which shows the entries' details and contains a delete button. When clicking the |
248 | | - delete button, the user is redirected back to the *overview page*. |
249 | | - |
250 | | -```js |
251 | | -beforeEach(function () { |
252 | | - var deleteEntry = function () { |
253 | | - browser().navigateTo('/entries'); |
254 | | - |
255 | | - // we need to select the <tbody> element as it might be the case that there |
256 | | - // are no entries (and therefore no rows). When the selector does not |
257 | | - // result in a match, the test would be marked as a failure. |
258 | | - element('table tbody').query(function (tbody, done) { |
259 | | - // ngScenario gives us a jQuery lite wrapped element. We call the |
260 | | - // `children()` function to retrieve the table body's rows |
261 | | - var children = tbody.children(); |
262 | | - |
263 | | - if (children.length > 0) { |
264 | | - // if there is at least one entry in the table, click on the link to |
265 | | - // the entry's detail view |
266 | | - element('table tbody a').click(); |
267 | | - // and, after a route change, click the delete button |
268 | | - element('.btn-danger').click(); |
269 | | - } |
270 | | - |
271 | | - // if there is more than one entry shown in the table, queue another |
272 | | - // delete action. |
273 | | - if (children.length > 1) { |
274 | | - deleteEntry(); |
275 | | - } |
276 | | - |
277 | | - // remember to call `done()` so that ngScenario can continue |
278 | | - // test execution. |
279 | | - done(); |
280 | | - }); |
281 | | - |
282 | | - }; |
283 | | - |
284 | | - // start deleting entries |
285 | | - deleteEntry(); |
| 68 | + // Verify that now there is only one item in the task list |
| 69 | + expect(element.all(by.repeater('task in tasks')).count()).toEqual(1); |
| 70 | + }); |
286 | 71 | }); |
287 | 72 | ``` |
288 | 73 |
|
289 | | -In order to understand what is happening, we should emphasize that ngScenario calls are not |
290 | | -immediately executed, but queued (in ngScenario terms, we would be talking about adding |
291 | | -future actions). If we had only one entry in our table, then the following future actions |
292 | | -would be queued: |
293 | | - |
294 | | -```js |
295 | | -// delete entry 1 |
296 | | -browser().navigateTo('/entries'); |
297 | | -element('table tbody').query(function (tbody, done) { ... }); |
298 | | -element('table tbody a'); |
299 | | -element('.btn-danger').click(); |
300 | | -``` |
301 | | - |
302 | | -For two entries, ngScenario would have to work on the following queue: |
| 74 | +This test describes the requirements of a ToDo list, specifically, that it should be able to |
| 75 | +filter the list of items. |
303 | 76 |
|
304 | | -```js |
305 | | -// delete entry 1 |
306 | | -browser().navigateTo('/entries'); |
307 | | -element('table tbody').query(function (tbody, done) { ... }); |
308 | | -element('table tbody a'); |
309 | | -element('.btn-danger').click(); |
310 | | - |
311 | | - // delete entry 2 |
312 | | - // indented to represent "recursion depth" |
313 | | - browser().navigateTo('/entries'); |
314 | | - element('table tbody').query(function (tbody, done) { ... }); |
315 | | - element('table tbody a'); |
316 | | - element('.btn-danger').click(); |
317 | | -``` |
| 77 | +## Example |
| 78 | +See the [angular-seed](https://github.com/angular/angular-seed) project for more examples, or look |
| 79 | +at the embedded examples in the Angular documentation (For example, [$http](http://docs.angularjs.org/api/ng/service/$http) |
| 80 | +has an end to end test in the example under the `protractor.js` tag). |
318 | 81 |
|
319 | 82 | ## Caveats |
320 | 83 |
|
321 | | -`ngScenario` does not work with apps that manually bootstrap using `angular.bootstrap`. You must use the `ng-app` directive. |
| 84 | +Protractor does not work out-of-the-box with apps that manually bootstrap manually using |
| 85 | +`angular.bootstrap`. You must use the `ng-app` directive. |
0 commit comments