Merge lp:~nskaggs/autopilot/page-object-docs into lp:autopilot

Proposed by Nicholas Skaggs
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
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

Description of the change

Convert https://developer.ubuntu.com/en/apps/platform/guides/acceptance-testing-using-the-page-object-model/ into the autopilot documentation.

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!

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
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".

review: Needs Fixing
Revision history for this message
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.

review: Needs Fixing
Revision history for this message
Christopher Lee (veebers) wrote :

Having a separate page is a good idea, perhaps a "case study" or a suggestion for structed testing for applications.

review: Needs Fixing
Revision history for this message
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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:522
http://jenkins.qa.ubuntu.com/job/autopilot-ci/986/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-amd64-ci/49
        deb: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-amd64-ci/49/artifact/work/output/*zip*/output.zip
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-armhf-ci/49
        deb: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-armhf-ci/49/artifact/work/output/*zip*/output.zip
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-i386-ci/49
        deb: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-i386-ci/49/artifact/work/output/*zip*/output.zip
    UNSTABLE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-vivid-touch/912
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-vivid-autopilot/65
    UNSTABLE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-runner-vivid-mako/803
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-armhf/910
        deb: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-armhf/910/artifact/work/output/*zip*/output.zip
    SUCCESS: http://s-jenkins.ubuntu-ci:8080/job/touch-flash-device/17306
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-testrunner-otto-vivid-autopilot/70
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-amd64/489
        deb: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-amd64/489/artifact/work/output/*zip*/output.zip

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/autopilot-ci/986/rebuild

review: Approve (continuous-integration)
Revision history for this message
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

Revision history for this message
Nicholas Skaggs (nskaggs) wrote :

This is ready for review again :-)

525. By Nicholas Skaggs

fix trunk issue

Revision history for this message
Christopher Lee (veebers) wrote :

(comment about warning when building removed as it's fixed.)

Looking good, jut a couple of minor things pointed out.

review: Needs Fixing
526. By Nicholas Skaggs

rebase trunk

527. By Nicholas Skaggs

fixes for veebers

528. By Nicholas Skaggs

add ...

Revision history for this message
Christopher Lee (veebers) wrote :

Awesome, LGTM. Thanks for that.

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:528
http://jenkins.qa.ubuntu.com/job/autopilot-ci/993/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-amd64-ci/56
        deb: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-amd64-ci/56/artifact/work/output/*zip*/output.zip
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-armhf-ci/56
        deb: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-armhf-ci/56/artifact/work/output/*zip*/output.zip
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-i386-ci/56
        deb: http://jenkins.qa.ubuntu.com/job/autopilot-vivid-i386-ci/56/artifact/work/output/*zip*/output.zip
    UNSTABLE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-vivid-touch/921
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-vivid-autopilot/72
    UNSTABLE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-runner-vivid-mako/812
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-armhf/919
        deb: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-armhf/919/artifact/work/output/*zip*/output.zip
    SUCCESS: http://s-jenkins.ubuntu-ci:8080/job/touch-flash-device/17324
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-testrunner-otto-vivid-autopilot/77
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-amd64/497
        deb: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-amd64/497/artifact/work/output/*zip*/output.zip

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/autopilot-ci/993/rebuild

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches