Merge lp:~nskaggs/autopilot/page-object-docs into lp:autopilot
- page-object-docs
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Christopher Lee |
Approved revision: | 528 |
Merged at revision: | 530 |
Proposed branch: | lp:~nskaggs/autopilot/page-object-docs |
Merge into: | lp:autopilot |
Diff against target: |
257 lines (+231/-0) 3 files modified
docs/contents.rst (+1/-0) docs/guides/page_object.rst (+226/-0) docs/tutorial/good_tests.rst (+4/-0) |
To merge this branch: | bzr merge lp:~nskaggs/autopilot/page-object-docs |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Christopher Lee (community) | Approve | ||
Richard Huddie (community) | Needs Fixing | ||
Allan LeSage (community) | Needs Fixing | ||
Review via email: mp+246504@code.launchpad.net |
Commit message
Convert https:/
Description of the change
Convert https:/
Open questions:
I didn't convert all of the text as it was originally intended as a guide and didn't fit well within the theme and voice of the 'writing good tests' section of autopilot. I also considered creating it as a separate page. Opinions welcome!
PS Jenkins bot (ps-jenkins) wrote : | # |
Allan LeSage (allanlesage) wrote : | # |
A couple of improvements possible :) , wonder if we want to make an item for "study the Ubuntu SDK or UI Toolkit helpers".
Richard Huddie (rhuddie) wrote : | # |
This looks good. I've made a couple of minor points below.
I was also thinking, should we mention about only driving the UI input from the page object helpers, and not directly from the test itself? This may be mentioned somewhere else, but I just thought about it when reading through this. It's probably covered under making tests less flaky as all the UI input is driven by the helpers, not repeated throughout different tests.
Christopher Lee (veebers) wrote : | # |
Having a separate page is a good idea, perhaps a "case study" or a suggestion for structed testing for applications.
Nicholas Skaggs (nskaggs) wrote : | # |
I addressed many of the comments, but left the text on the same page for now. I'll look at what the structure would look like for migrating it out to a separate page. My original plan was to leave the small paragraph on 'think about design' in the good tests, and link to the remainder.
- 522. By Nicholas Skaggs
-
address MP comments
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:522
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
UNSTABLE: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
Nicholas Skaggs (nskaggs) wrote : | # |
Adding the major content to it's own page in the new guides section.
- 523. By Nicholas Skaggs
-
move to guides
- 524. By Nicholas Skaggs
-
fix guides location
Nicholas Skaggs (nskaggs) wrote : | # |
This is ready for review again :-)
- 525. By Nicholas Skaggs
-
fix trunk issue
Christopher Lee (veebers) wrote : | # |
(comment about warning when building removed as it's fixed.)
Looking good, jut a couple of minor things pointed out.
- 526. By Nicholas Skaggs
-
rebase trunk
- 527. By Nicholas Skaggs
-
fixes for veebers
- 528. By Nicholas Skaggs
-
add ...
Christopher Lee (veebers) wrote : | # |
Awesome, LGTM. Thanks for that.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:528
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
UNSTABLE: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'docs/contents.rst' |
2 | --- docs/contents.rst 2014-05-20 14:02:18 +0000 |
3 | +++ docs/contents.rst 2015-01-22 01:16:09 +0000 |
4 | @@ -9,6 +9,7 @@ |
5 | tutorial/tutorial |
6 | api/index |
7 | porting/porting |
8 | + guides/page_object |
9 | faq/faq |
10 | faq/contribute |
11 | faq/troubleshooting |
12 | |
13 | === added directory 'docs/guides' |
14 | === added file 'docs/guides/page_object.rst' |
15 | --- docs/guides/page_object.rst 1970-01-01 00:00:00 +0000 |
16 | +++ docs/guides/page_object.rst 2015-01-22 01:16:09 +0000 |
17 | @@ -0,0 +1,226 @@ |
18 | +.. _page_object_guide: |
19 | + |
20 | +Page Object Pattern |
21 | +#################### |
22 | + |
23 | +.. contents:: |
24 | + |
25 | +Introducing the Page Object Pattern |
26 | +----------------------------------- |
27 | +Automated testing of an application through the Graphical User Interface (GUI) is inherently fragile. |
28 | +These tests require regular review and attention during the development cycle. This is known as Interface Sensitivity (`"even minor changes to the interface can cause tests to fail" <https://books.google.com/books?isbn=0132797461>`_). |
29 | +Utilizing the page-object pattern, alleviates some of the problems stemming from this fragility, allowing us to do automated user acceptance testing (UAT) in a sustainable manner. |
30 | + |
31 | +The Page Object Pattern comes from the `Selenium community <https://code.google.com/p/selenium/wiki/PageObjects>`_ and is the best way to turn a flaky and unmaintainable user acceptance test into a stable and useful |
32 | +part of your release process. A page is what's visible on the screen at a single moment. |
33 | +A user story consists of a user jumping from page to page until they achieve their goal. |
34 | +Thus pages are modeled as objects following these guidelines: |
35 | + |
36 | +#. The public methods represent the services that the page offers. |
37 | +#. Try not to expose the internals of the page. |
38 | +#. Methods return other PageObjects. |
39 | +#. Assertions should exist only in tests |
40 | +#. Objects need not represent the entire page. |
41 | +#. Actions which produce multiple results should have a test for each result |
42 | + |
43 | +Lets take the page objects of the `Ubuntu Clock App <http://bazaar.launchpad.net/~ubuntu-clock-dev/ubuntu-clock-app/trunk/view/399/tests/autopilot/ubuntu_clock_app/emulators.py>`__ as an example, with some simplifications. This application is written in |
44 | +QML and Javascript using the `Ubuntu SDK <http://developer.ubuntu.com/apps/sdk/>`__. |
45 | + |
46 | +1. The public methods represent the services that the page offers. |
47 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
48 | + |
49 | +This application has a stopwatch page that lets users measure elapsed |
50 | +time. It offers services to start, stop and reset the watch, so we start |
51 | +by defining the stop watch page object as follows: |
52 | + |
53 | +.. code-block:: python |
54 | + |
55 | + class Stopwatch(object): |
56 | + |
57 | + def start(self): |
58 | + raise NotImplementedError() |
59 | + |
60 | + def stop(self): |
61 | + raise NotImplementedError() |
62 | + |
63 | + def reset(self): |
64 | + raise NotImplementedError() |
65 | + |
66 | +2. Try not to expose the internals of the page. |
67 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
68 | + |
69 | +The internals of the page are more likely to change than the services it |
70 | +offers. A stopwatch will keep the same three services we defined above |
71 | +even if the whole design changes. In this case, we reset the stop watch |
72 | +by clicking a button on the bottom-left of the window, but we hide that |
73 | +as an implementation detail behind the public methods. In Python, we can |
74 | +indicate that a method is for internal use only by adding a single |
75 | +leading underscore to its name. So, lets implement the reset\_stopwatch |
76 | +method: |
77 | + |
78 | +.. code-block:: python |
79 | + |
80 | + def reset(self): |
81 | + self._click_reset_button() |
82 | + |
83 | + def _click_reset_button(self): |
84 | + reset_button = self.wait_select_single( |
85 | + 'ImageButton', objectName='resetButton') |
86 | + self.pointing_device.click_object(reset_button) |
87 | + |
88 | +Now if the designers go crazy and decide that it's better to reset the |
89 | +stop watch in a different way, we will have to make the change only in |
90 | +one place to keep all the tests working. Remember that this type of |
91 | +tests has Interface Sensitivity, that's unavoidable; but we can reduce |
92 | +the impact of interface changes with proper encapsulation and turn these |
93 | +tests into a useful way to verify that a change in the GUI didn't |
94 | +introduce any regressions. |
95 | + |
96 | +.. _page_object_guide_guideline_3: |
97 | + |
98 | +3. Methods return other PageObjects |
99 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
100 | + |
101 | +An UAT checks a user story. It will involve the journey of the user |
102 | +through the system, so he will move from one page to another. Lets take |
103 | +a look at how a journey to reset the stop watch will look like: |
104 | + |
105 | +.. code-block:: python |
106 | + |
107 | + stopwatch = clock_page.open_stopwatch() |
108 | + stopwatch.start() |
109 | + stopwatch.reset() |
110 | + |
111 | +In our sample application, the first page that the user will encounter |
112 | +is the Clock. One of the things the user can do from this page is to |
113 | +open the stopwatch page, so we model that as a service that the Clock |
114 | +page provides. Then return the new page object that will be visible to |
115 | +the user after completing that step. |
116 | + |
117 | +.. code-block:: python |
118 | + |
119 | + class Clock(object): |
120 | + |
121 | + ... |
122 | + |
123 | + def open_stopwatch(self): |
124 | + self._switch_to_tab('StopwatchTab') |
125 | + return self.wait_select_single(Stopwatch) |
126 | + |
127 | +Now the return value of open\_stopwatch will make available to the |
128 | +caller all the available services that the stopwatch exposes to the |
129 | +user. Thus it can be chained as a user journey from one page to the |
130 | +other. |
131 | + |
132 | +4. Assertions should exist only in tests |
133 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
134 | + |
135 | +A well written UAT consists of a sequence of |
136 | +steps or user actions and ends with one single assertion that verifies |
137 | +that the user achieved its goal. The page objects are the helpers for |
138 | +the user actions part of the test, so it's better to leave the check for |
139 | +success out of them. With that in mind, a test for the reset of the |
140 | +stopwatch would look like this: |
141 | + |
142 | +.. code-block:: python |
143 | + |
144 | + def test_restart_button_must_restart_stopwatch_time(self): |
145 | + # Set up. |
146 | + stopwatch = self.clock_page.open_stopwatch() |
147 | + |
148 | + stopwatch.start() |
149 | + stopwatch.reset_stopwatch() |
150 | + |
151 | + # Check that the stopwatch has been reset. |
152 | + self.assertThat( |
153 | + stopwatch.get_time, |
154 | + Eventually(Equals('00:00.0'))) |
155 | + |
156 | +We have to add a new method to the stopwatch page object: get\_time. But |
157 | +it only returns the state of the GUI as the user sees it. We leave in |
158 | +the test method the assertion that checks it's the expected value. |
159 | + |
160 | +.. code-block:: python |
161 | + |
162 | + class Stopwatch(object): |
163 | + |
164 | + ... |
165 | + |
166 | + def get_time(self): |
167 | + return self.wait_select_single( |
168 | + 'Label', objectName='time').text |
169 | + |
170 | +5. Need not represent an entire page |
171 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
172 | + |
173 | +The objects we are modeling here can just represent a part of the page. |
174 | +Then we build the entire page that the user is seeing by composition of |
175 | +page parts. This way we can reuse test code for parts of the GUI that |
176 | +are reused in the application or between different applications. As an |
177 | +example, take the \_switch\_to\_tab('StopwatchTab') method that we are |
178 | +using to open the stopwatch page. The Clock application is using the |
179 | +Header component provided by the Ubuntu SDK, as all the other Ubuntu |
180 | +applications are doing too. So, the Ubuntu SDK also provides helpers to |
181 | +make it easier the user acceptance testing of the applications, and you |
182 | +will find an object like this: |
183 | + |
184 | +.. code-block:: python |
185 | + |
186 | + class Header(object): |
187 | + |
188 | + def switch_to_tab(tab_object_name): |
189 | + """Open a tab. |
190 | + |
191 | + :parameter tab_object_name: The QML objectName property of the tab. |
192 | + :return: The newly opened tab. |
193 | + :raise ToolkitException: If there is no tab with that object |
194 | + name. |
195 | + |
196 | + """ |
197 | + ... |
198 | + |
199 | +This object just represents the header of the page, and inside the |
200 | +object we define the services that the header provides to the users. If |
201 | +you dig into the full implementation of the Clock test class you will |
202 | +find that in order to open the stopwatch page we end up calling Header |
203 | +methods. |
204 | + |
205 | +6. Actions which produce multiple results should have a test for each result |
206 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
207 | + |
208 | +According to the guideline :ref:`page_object_guide_guideline_3`, we are returning page objects every time |
209 | +that a user action opens the option for new actions to execute. |
210 | +Sometimes the same action has different results depending on the context |
211 | +or the values used for the action. For example, the Clock app has an |
212 | +Alarm page. In this page you can add new alarms, but if you try to add |
213 | +an alarm for sometime in the past, it will result in an error. So, we |
214 | +will have two different tests that will look something like this: |
215 | + |
216 | +.. code-block:: python |
217 | + |
218 | + def test_add_alarm_for_tomorrow_must_add_to_alarm_list(self): |
219 | + tomorrow = ... |
220 | + test_alarm_name = 'Test alarm for tomorrow' |
221 | + alarm_page = self.alarm_page.add_alarm( |
222 | + test_alarm_name, tomorrow) |
223 | + |
224 | + saved_alarms = alarm_page.get_saved_alarms() |
225 | + self.assertIn( |
226 | + (test_alarm_name, tomorrow), |
227 | + saved_alarms) |
228 | + |
229 | + def test_add_alarm_for_earlier_today_must_display_error(self): |
230 | + earlier_today = ... |
231 | + test_alarm_name = 'Test alarm for earlier_today' |
232 | + error_dialog = self.alarm_page.add_alarm_with_error( |
233 | + test_alarm_name, earlier_today) |
234 | + |
235 | + self.assertEqual( |
236 | + error_dialog.text, |
237 | + 'Please select a time in the future.') |
238 | + |
239 | +Take a look at the methods add\_alarm and add\_alarm\_with\_error. The |
240 | +first one returns the Alarm page again, where the user can continue his |
241 | +journey or finish the test checking the result. The second one returns |
242 | +the error dialog that's expected when you try to add an alarm with the |
243 | +wrong values. |
244 | |
245 | === modified file 'docs/tutorial/good_tests.rst' |
246 | --- docs/tutorial/good_tests.rst 2015-01-19 22:48:14 +0000 |
247 | +++ docs/tutorial/good_tests.rst 2015-01-22 01:16:09 +0000 |
248 | @@ -98,6 +98,10 @@ |
249 | 5. Commit. Push. Superseed original merge proposal with your branch. |
250 | 6. Celebrate! |
251 | |
252 | +Think about design |
253 | +++++++++++++++++++ |
254 | +Much in the same way you might choose a functional or objective-oriented paradigm for a piece of code, a testsuite can benefit from choosing a good design pattern. One such design pattern is the page object model. The page object model can reduce testcase complexity and allow the testcase to grow and easily adapt to changes within the underlying application. Check out :ref:`page_object_guide`. |
255 | + |
256 | Test Length |
257 | +++++++++++ |
258 |
FAILED: Continuous integration, rev:521 jenkins. qa.ubuntu. com/job/ autopilot- ci/947/ jenkins. qa.ubuntu. com/job/ autopilot- vivid-amd64- ci/10 jenkins. qa.ubuntu. com/job/ autopilot- vivid-amd64- ci/10/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ autopilot- vivid-armhf- ci/10 jenkins. qa.ubuntu. com/job/ autopilot- vivid-armhf- ci/10/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ autopilot- vivid-i386- ci/10 jenkins. qa.ubuntu. com/job/ autopilot- vivid-i386- ci/10/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ generic- mediumtests- vivid-autopilot /14 jenkins. qa.ubuntu. com/job/ autopilot- testrunner- otto-vivid- autopilot/ 21 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-amd64/ 405 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-amd64/ 405/artifact/ work/output/ *zip*/output. zip
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
UNSTABLE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/autopilot- ci/947/ rebuild
http://