Merge lp:~nskaggs/autopilot/apdocs-tutorial into lp:autopilot
- apdocs-tutorial
- Merge into trunk
Proposed by
Nicholas Skaggs
Status: | Work in progress |
---|---|
Proposed branch: | lp:~nskaggs/autopilot/apdocs-tutorial |
Merge into: | lp:autopilot |
Diff against target: |
534 lines (+197/-246) 2 files modified
docs/tutorial/getting_started.rst (+196/-245) docs/tutorial/what_is_autopilot.rst (+1/-1) |
To merge this branch: | bzr merge lp:~nskaggs/autopilot/apdocs-tutorial |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Autopilot Hackers | Pending | ||
Review via email: mp+247218@code.launchpad.net |
Commit message
Rebase autopilot tutorial to use qml
Description of the change
Rebase autopilot tutorial to use qml
To post a comment you must log in.
- 524. By Nicholas Skaggs
-
remerge trunk
- 525. By Nicholas Skaggs
-
rebase trunk
- 526. By Nicholas Skaggs
-
wip
- 527. By Nicholas Skaggs
-
rebase trunk
- 528. By Nicholas Skaggs
-
rebase trunk
- 529. By Nicholas Skaggs
-
fix tutorial
Unmerged revisions
- 529. By Nicholas Skaggs
-
fix tutorial
- 528. By Nicholas Skaggs
-
rebase trunk
- 527. By Nicholas Skaggs
-
rebase trunk
- 526. By Nicholas Skaggs
-
wip
- 525. By Nicholas Skaggs
-
rebase trunk
- 524. By Nicholas Skaggs
-
remerge trunk
- 523. By Nicholas Skaggs
-
wip
- 522. By Nicholas Skaggs
-
wip; initial tutorial work
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'docs/tutorial/getting_started.rst' | |||
2 | --- docs/tutorial/getting_started.rst 2015-01-25 23:08:51 +0000 | |||
3 | +++ docs/tutorial/getting_started.rst 2015-01-27 20:14:32 +0000 | |||
4 | @@ -3,78 +3,47 @@ | |||
5 | 3 | 3 | ||
6 | 4 | This document contains everything you need to know to write your first autopilot test. It covers writing several simple tests for a sample Qt5/Qml application. However, it's important to note that nothing in this tutorial is specific to Qt5/Qml, and will work equally well with any other kind of application. | 4 | This document contains everything you need to know to write your first autopilot test. It covers writing several simple tests for a sample Qt5/Qml application. However, it's important to note that nothing in this tutorial is specific to Qt5/Qml, and will work equally well with any other kind of application. |
7 | 5 | 5 | ||
22 | 6 | Files and Directories | 6 | Prerequisites |
23 | 7 | ===================== | 7 | ============= |
24 | 8 | 8 | For the tutorial you will need to ensure you have installed autopilot. See :ref:`installing_autopilot` for installation instructions. | |
25 | 9 | Your autopilot test suite will grow to several files, possibly spread across several directories. We recommend that you follow this simple directory layout:: | 9 | |
26 | 10 | 10 | In addition, if you wish to follow along directly with the tutorial you should install the ubuntu sdk. See `developer.ubuntu.com <https://developer.ubuntu.com/en/start/ubuntu-sdk/installing-the-sdk/>`_ for installation instructions. Since you can apply the tutorial to any application introspectable by autopilot this step is optional. | |
13 | 11 | autopilot/ | ||
14 | 12 | autopilot/<projectname>/ | ||
15 | 13 | autopilot/<projectname>/tests/ | ||
16 | 14 | |||
17 | 15 | The ``autopilot`` folder can be anywhere within your project's source tree. It will likely contain a `setup.py <http://docs.python.org/3/distutils/setupscript.html>`_ file. | ||
18 | 16 | |||
19 | 17 | The ``autopilot/<projectname>/`` folder is the base package for your autopilot tests. This folder, and all child folders, are python packages, and so must contain an `__init__.py file <http://docs.python.org/3/tutorial/modules.html#packages>`_. If you ever find yourself writing custom proxy classes (This is an advanced topic, and is covered here: :ref:`custom_proxy_classes`), they should be imported from this top-level package. | ||
20 | 18 | |||
21 | 19 | Each test file should be named ``test_<component>.py``, where *<component>* is the logical component you are testing in that file. Test files must be written in the ``autopilot/<projectname>/tests/`` folder. | ||
27 | 20 | 11 | ||
28 | 21 | A Minimal Test Case | 12 | A Minimal Test Case |
29 | 22 | +++++++++++++++++++ | 13 | +++++++++++++++++++ |
30 | 23 | 14 | ||
41 | 24 | Autopilot tests follow a similar pattern to other python test libraries: you must declare a class that derives from :class:`~autopilot.testcase.AutopilotTestCase`. A minimal test case looks like this:: | 15 | Autopilot tests follow a similar pattern to other python test libraries: you must declare a class that derives from :class:`~autopilot.testcase.AutopilotTestCase`. The *AutopilotTestCase* class contains methods to launch and introspect and application, along with the ability to make assertions. Thus, a minimal test case looks like this:: |
42 | 25 | 16 | ||
43 | 26 | from autopilot.testcase import AutopilotTestCase | 17 | from autopilot.testcase import AutopilotTestCase |
44 | 27 | 18 | ||
45 | 28 | 19 | ||
46 | 29 | class MyTests(AutopilotTestCase): | 20 | class MyTests(AutopilotTestCase): |
47 | 30 | 21 | ||
48 | 31 | def test_something(self): | 22 | def test_something(self): |
49 | 32 | """An example test case that will always pass.""" | 23 | """An example test case that will always pass.""" |
50 | 33 | self.assertTrue(True) | 24 | self.assertTrue(True) |
51 | 34 | 25 | ||
52 | 35 | .. otto:: **Make your tests expressive!** | 26 | .. otto:: **Make your tests expressive!** |
53 | 36 | 27 | ||
80 | 37 | It's important to make sure that your tests express your *intent* as clearly as possible. We recommend choosing long, descriptive names for test functions and classes (even breaking :pep:`8`, if you need to), and give your tests a detailed docstring explaining exactly what you are trying to test. For more detailed advice on this point, see :ref:`write-expressive-tests` | 28 | It's important to make sure that your tests express your *intent* as clearly as possible. We recommend choosing long, descriptive names for test functions and classes (even breaking :pep:`8`, if you need to), and give your tests a detailed docstring explaining exactly what you are trying to test. For more detailed advice on this point, see :ref:`write-expressive-tests` |
81 | 38 | 29 | ||
56 | 39 | The Setup Phase | ||
57 | 40 | =============== | ||
58 | 41 | |||
59 | 42 | Before each test is run, the ``setUp`` method is called. Test authors may override this method to run any setup that needs to happen before the test is run. However, care must be taken when using the ``setUp`` method: it tends to hide code from the test case, which can make your tests less readable. It is our recommendation, therefore, that you use this feature sparingly. A more suitable alternative is often to put the setup code in a separate function or method and call it from the test function. | ||
60 | 43 | |||
61 | 44 | Should you wish to put code in a setup method, it looks like this: | ||
62 | 45 | |||
63 | 46 | .. code-block:: python | ||
64 | 47 | |||
65 | 48 | from autopilot.testcase import AutopilotTestCase | ||
66 | 49 | |||
67 | 50 | |||
68 | 51 | class MyTests(AutopilotTestCase): | ||
69 | 52 | |||
70 | 53 | def setUp(self): | ||
71 | 54 | super(MyTests, self).setUp() | ||
72 | 55 | # This code gets run before every test! | ||
73 | 56 | |||
74 | 57 | def test_something(self): | ||
75 | 58 | """An example test case that will always pass.""" | ||
76 | 59 | self.assertTrue(True) | ||
77 | 60 | |||
78 | 61 | .. note:: | ||
79 | 62 | Any action you take in the setup phase must be undone if it alters the system state. See :ref:`cleaning-up` for more details. | ||
82 | 63 | 30 | ||
83 | 64 | Starting the Application | 31 | Starting the Application |
84 | 65 | ++++++++++++++++++++++++ | 32 | ++++++++++++++++++++++++ |
85 | 66 | 33 | ||
87 | 67 | At the start of your test, you need to tell autopilot to launch your application. To do this, call :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application`. The minimum required argument to this method is the application name or path. If you pass in the application name, autopilot will look in the current working directory, and then will search the :envvar:`PATH` environment variable. Otherwise, autopilot looks for the executable at the path specified. Positional arguments to this method are passed to the executable being launched. | 34 | At the start of your test, you need to tell autopilot to launch your application. To do this, call :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application`. The minimum required argument to this method is the application name or path. If you pass in the application name, autopilot will look in the current working directory, and then will search the :envvar:`PATH` environment variable. Otherwise, autopilot looks for the executable at the path specified. Positional arguments to this method are passed to the executable being launched. For example, we can launch our Qt5 qml application by launching qmlscene with the page to our qml file:: |
88 | 35 | |||
89 | 36 | self.launch_test_application('qmlscene', 'my_scene.qml') | ||
90 | 68 | 37 | ||
91 | 69 | Autopilot will try and guess what type of application you are launching, and therefore what kind of introspection libraries it should load. Sometimes autopilot will need some assistance however. For example, at the time of writing, autopilot cannot automatically detect the introspection type for python / Qt4 applications. In that case, a :class:`RuntimeError` will be raised. To provide autopilot with a hint as to which introspection type to load, you can provide the ``app_type`` keyword argument. For example:: | 38 | Autopilot will try and guess what type of application you are launching, and therefore what kind of introspection libraries it should load. Sometimes autopilot will need some assistance however. For example, at the time of writing, autopilot cannot automatically detect the introspection type for python / Qt4 applications. In that case, a :class:`RuntimeError` will be raised. To provide autopilot with a hint as to which introspection type to load, you can provide the ``app_type`` keyword argument. For example:: |
92 | 70 | 39 | ||
94 | 71 | class MyTests(AutopilotTestCase): | 40 | class MyTests(AutopilotTestCase): |
95 | 72 | 41 | ||
101 | 73 | def test_python_qt4_application(self): | 42 | def test_python_qt4_application(self): |
102 | 74 | self.app = self.launch_test_application( | 43 | self.app = self.launch_test_application( |
103 | 75 | 'my-pyqt4-app', | 44 | 'my-pyqt4-app', |
104 | 76 | app_type='qt' | 45 | app_type='qt' |
105 | 77 | ) | 46 | ) |
106 | 78 | 47 | ||
107 | 79 | See the documentation for :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application` for more details. | 48 | See the documentation for :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application` for more details. |
108 | 80 | 49 | ||
109 | @@ -82,169 +51,151 @@ | |||
110 | 82 | 51 | ||
111 | 83 | .. otto:: **What is a Proxy Object?** | 52 | .. otto:: **What is a Proxy Object?** |
112 | 84 | 53 | ||
116 | 85 | Whenever you launch an application, autopilot gives you a "proxy object". These are instances of the :class:`~autopilot.introspection.ProxyBase` class, with all the data from your application mirrored in the proxy object instances. For example, if you have a proxy object for a push button class (say, ``QPushButton``, for example), the proxy object will have attribute to match every attribute in the class within your application. Autopilot automatically keeps the data in these instances up to date, so you can use them in your test assertions. | 54 | Whenever you launch an application, autopilot gives you a "proxy object". These are instances of the :class:`~autopilot.introspection.ProxyBase` class, with all the data from your application mirrored in the proxy object instances. For example, if you have a proxy object for a push button class (say, ``QPushButton``, for example), the proxy object will have attribute to match every attribute in the class within your application. Autopilot automatically keeps the data in these instances up to date, so you can use them in your test assertions. |
117 | 86 | 55 | ||
118 | 87 | User interfaces are made up of a tree of widgets, and autopilot represents these widgets as a tree of proxy objects. Proxy objects have a number of methods on them for selecting child objects in the introspection tree, so test authors can easily inspect the parts of the UI tree they care about. | 56 | User interfaces are made up of a tree of widgets, and autopilot represents these widgets as a tree of proxy objects. Proxy objects have a number of methods on them for selecting child objects in the introspection tree, so test authors can easily inspect the parts of the UI tree they care about. Later in the tutorial using :meth:`~autopilot.introspection.ProxyBase.print_tree` and the vis tool (see :ref:`visualise_introspection_tree`) will show you ways to examine a UI tree. |
119 | 57 | |||
120 | 58 | Testcase Layout | ||
121 | 59 | =============== | ||
122 | 60 | |||
123 | 61 | Your autopilot test suite will grow to several files, possibly spread across several directories. We recommend that you follow this simple directory layout:: | ||
124 | 62 | |||
125 | 63 | autopilot/ | ||
126 | 64 | autopilot/<projectname>/ | ||
127 | 65 | autopilot/<projectname>/tests/ | ||
128 | 66 | |||
129 | 67 | The ``autopilot`` folder can be anywhere within your project's source tree. It will likely contain a `setup.py <http://docs.python.org/3/distutils/setupscript.html>`_ file. | ||
130 | 68 | |||
131 | 69 | The ``autopilot/<projectname>/`` folder is the base package for your autopilot tests. This folder, and all child folders, are python packages, and so must contain an `__init__.py file <http://docs.python.org/3/tutorial/modules.html#packages>`_. If you ever find yourself writing custom proxy classes (This is an advanced topic, and is covered here: :ref:`custom_proxy_classes`), they should be imported from this top-level package. | ||
132 | 70 | |||
133 | 71 | Each test file should be named ``test_<component>.py``, where *<component>* is the logical component you are testing in that file. Test files must be written in the ``autopilot/<projectname>/tests/`` folder. | ||
134 | 72 | |||
135 | 73 | So for example, here is a minimal layout following the above recommendations:: | ||
136 | 74 | |||
137 | 75 | autopilot/exampleproject/__init__.py | ||
138 | 76 | autopilot/exampleproject/tests/__init__.py | ||
139 | 77 | autopilot/exampleproject/tests/test_example_component.py | ||
140 | 78 | |||
141 | 79 | In the example above the **test_example_component.py** file would contain the actual testcases. | ||
142 | 80 | |||
143 | 88 | 81 | ||
144 | 89 | A Simple Test | 82 | A Simple Test |
145 | 90 | ============= | 83 | ============= |
146 | 91 | 84 | ||
192 | 92 | To demonstrate the material covered so far, this selection will outline a simple application, and a single test for it. Instead of testing a third-party application, we will write the simplest possible application in Python and Qt4. The application, named 'testapp.py', is listed below:: | 85 | To demonstrate the material covered so far, this selection will outline a simple application and a single test for it. Instead of testing a third-party application, we will write the simplest possible application using Qt5 and qml. The application, named 'testapp.qml', is listed below:: |
193 | 93 | 86 | ||
194 | 94 | #!/usr/bin/env python | 87 | import QtQuick 2.0 |
195 | 95 | 88 | import Ubuntu.Components 1.1 | |
196 | 96 | from PyQt4 import QtGui | 89 | |
197 | 97 | from sys import argv | 90 | MainView { |
198 | 98 | 91 | ||
199 | 99 | def main(): | 92 | width: units.gu(100) |
200 | 100 | app = QtGui.QApplication(argv) | 93 | height: units.gu(75) |
201 | 101 | win = QtGui.QMainWindow() | 94 | |
202 | 102 | win.show() | 95 | Page { |
203 | 103 | win.setWindowTitle("Hello World") | 96 | title: "Hello World" |
204 | 104 | app.exec_() | 97 | } |
205 | 105 | 98 | } | |
206 | 106 | if __name__ == '__main__': | 99 | |
207 | 107 | main() | 100 | |
208 | 108 | 101 | As you can see, this is a trivial application, but it serves our purpose. We will write a single autopilot test that asserts that the title of the main window is equal to the string "Hello World". Our test file is named "test_page.py", and contains the following code:: | |
209 | 109 | As you can see, this is a trivial application, but it serves our purpose. We will write a single autopilot test that asserts that the title of the main window is equal to the string "Hello World". Our test file is named "test_window.py", and contains the following code:: | 102 | |
210 | 110 | 103 | from autopilot.testcase import AutopilotTestCase | |
211 | 111 | from autopilot.testcase import AutopilotTestCase | 104 | from testtools.matchers import Equals |
212 | 112 | from os.path import abspath, dirname, join | 105 | |
213 | 113 | from testtools.matchers import Equals | 106 | |
214 | 114 | 107 | class TestPage(AutopilotTestCase): | |
215 | 115 | class MainWindowTitleTests(AutopilotTestCase): | 108 | |
216 | 116 | 109 | def launch_application(self): | |
217 | 117 | def launch_application(self): | 110 | return self.launch_test_application( |
218 | 118 | """Work out the full path to the application and launch it. | 111 | 'qmlscene', |
219 | 119 | 112 | '../../testapp.qml', | |
220 | 120 | This is necessary since our test application will not be in $PATH. | 113 | app_type='qt') |
221 | 121 | 114 | ||
222 | 122 | :returns: The application proxy object. | 115 | def test_page_title(self): |
223 | 123 | 116 | """The main page title must be 'Hello World'.""" | |
224 | 124 | """ | 117 | app = self.launch_application() |
225 | 125 | full_path = abspath(join(dirname(__file__), '..', '..', 'testapp.py')) | 118 | page = app.select_single('Page11') |
226 | 126 | return self.launch_test_application(full_path, app_type='qt') | 119 | |
227 | 127 | 120 | self.assertThat(page.title, Equals("Hello World")) | |
228 | 128 | def test_main_window_title_string(self): | 121 | |
229 | 129 | """The main window title must be 'Hello World'.""" | 122 | |
230 | 130 | app_root = self.launch_application() | 123 | Note that we have made the test method as readable as possible by hiding the complexities of finding the full path to the application we want to test (passing ../../testapp.qml). Of course, if you can guarantee that the application is in :envvar:`PATH`, then this step becomes a lot simpler. |
186 | 131 | main_window = app_root.select_single('QMainWindow') | ||
187 | 132 | |||
188 | 133 | self.assertThat(main_window.windowTitle, Equals("Hello World")) | ||
189 | 134 | |||
190 | 135 | |||
191 | 136 | Note that we have made the test method as readable as possible by hiding the complexities of finding the full path to the application we want to test. Of course, if you can guarantee that the application is in :envvar:`PATH`, then this step becomes a lot simpler. | ||
231 | 137 | 124 | ||
232 | 138 | The entire directory structure looks like this:: | 125 | The entire directory structure looks like this:: |
233 | 139 | 126 | ||
238 | 140 | ./example/__init__.py | 127 | ./testapp.qml |
239 | 141 | ./example/tests/__init__.py | 128 | ./autopilotqml/__init__.py |
240 | 142 | ./example/tests/test_window.py | 129 | ./autopilotqml/tests/__init__.py |
241 | 143 | ./testapp.py | 130 | ./autopilotqml/tests/test_page.py |
242 | 144 | 131 | ||
243 | 145 | The ``__init__.py`` files are empty, and are needed to make these directories importable by python. | 132 | The ``__init__.py`` files are empty, and are needed to make these directories importable by python. |
244 | 146 | 133 | ||
245 | 147 | Running Autopilot | 134 | Running Autopilot |
246 | 148 | +++++++++++++++++ | 135 | +++++++++++++++++ |
247 | 149 | 136 | ||
257 | 150 | From the root of this directory structure, we can ask autopilot to list all the tests it can find:: | 137 | From the root of this directory structure, we can ask autopilot to list all the tests it can find. Note that we use our python package name, which is the same as the root folder of the tests:: |
258 | 151 | 138 | ||
259 | 152 | $ autopilot3 list example | 139 | $ autopilot3 list autopilotqml |
260 | 153 | Loading tests from: /home/thomi/code/canonical/autopilot/example_test | 140 | Loading tests from: /home/nskaggs/projects/ubuntutouch/autopilotqml/tests/autopilot |
261 | 154 | 141 | ||
262 | 155 | example.tests.test_window.MainWindowTitleTests.test_main_window_title_string | 142 | autopilotqml.test_main.TestPage.test_page_title |
263 | 156 | 143 | ||
264 | 157 | 144 | 1 total tests. | |
256 | 158 | 1 total tests. | ||
265 | 159 | 145 | ||
266 | 160 | Note that on the first line, autopilot will tell you where it has loaded the test definitions from. Autopilot will look in the current directory for a python package that matches the package name specified on the command line. If it does not find any suitable packages, it will look in the standard python module search path instead. | 146 | Note that on the first line, autopilot will tell you where it has loaded the test definitions from. Autopilot will look in the current directory for a python package that matches the package name specified on the command line. If it does not find any suitable packages, it will look in the standard python module search path instead. |
267 | 161 | 147 | ||
268 | 162 | To run our test, we use the autopilot 'run' command:: | 148 | To run our test, we use the autopilot 'run' command:: |
269 | 163 | 149 | ||
277 | 164 | $ autopilot3 run example | 150 | $ autopilot3 run autopilotqml |
278 | 165 | Loading tests from: /home/thomi/code/canonical/autopilot/example_test | 151 | Loading tests from: /home/nskaggs/projects/ubuntutouch/autopilotqml/tests/autopilot |
279 | 166 | 152 | ||
280 | 167 | Tests running... | 153 | Tests running... |
281 | 168 | 154 | ||
282 | 169 | Ran 1 test in 2.342s | 155 | Ran 1 test in 2.503s |
283 | 170 | OK | 156 | OK |
284 | 171 | 157 | ||
285 | 172 | You will notice that the test application launches, and then dissapears shortly afterwards. Since this test doesn't manipulate the application in any way, this is a rather boring test to look at. If you ever want more output from the run command, you may specify the '-v' flag:: | 158 | You will notice that the test application launches, and then dissapears shortly afterwards. Since this test doesn't manipulate the application in any way, this is a rather boring test to look at. If you ever want more output from the run command, you may specify the '-v' flag:: |
286 | 173 | 159 | ||
307 | 174 | $ autopilot3 run -v example | 160 | $ autopilot3 run -v autopilotqml |
308 | 175 | Loading tests from: /home/thomi/code/canonical/autopilot/example_test | 161 | 12:07:50.313 INFO run:235 - Autopilot Source Version: 1.5.0 |
309 | 176 | 162 | Autopilot Package Version: 1.5.0+15.04.20141031-0ubuntu1 | |
310 | 177 | Tests running... | 163 | Loading tests from: /home/nskaggs/projects/ubuntutouch/autopilotqml/tests/autopilot |
311 | 178 | 13:41:11.614 INFO globals:49 - ************************************************************ | 164 | |
312 | 179 | 13:41:11.614 INFO globals:50 - Starting test example.tests.test_window.MainWindowTitleTests.test_main_window_title_string | 165 | Tests running... |
313 | 180 | 13:41:11.693 INFO __init__:136 - Launching process: ['/home/thomi/code/canonical/autopilot/example_test/testapp.py', '-testability'] | 166 | 12:07:50.323 INFO _logging:40 - ************************************************************ |
314 | 181 | 13:41:11.699 INFO __init__:169 - Looking for autopilot interface for PID 12013 (and children) | 167 | 12:07:50.323 INFO _logging:41 - Starting test autopilotqml.test_main.TestPage.test_page_title |
315 | 182 | 13:41:11.727 WARNING __init__:185 - Caught exception while searching for autopilot interface: 'DBusException("Could not get PID of name 'org.freedesktop.DBus': no such name",)' | 168 | 12:07:50.528 INFO _launcher:373 - Attempting to launch application 'qmlscene' with arguments '../../testapp.qml' as a normal process |
316 | 183 | 13:41:12.773 WARNING __init__:185 - Caught exception while searching for autopilot interface: 'DBusException("Could not get PID of name 'org.freedesktop.DBus': no such name",)' | 169 | 12:07:50.532 INFO _launcher:431 - Launching process: ['/usr/bin/qmlscene', '-testability', '../../testapp.qml'] |
317 | 184 | 13:41:12.848 WARNING __init__:185 - Caught exception while searching for autopilot interface: 'RuntimeError("Could not find Autopilot interface on DBus backend '<session bus :1.5967 /com/canonical/Autopilot/Introspection>'",)' | 170 | 12:07:51.739 INFO _launcher:544 - waiting for process to exit. |
318 | 185 | 13:41:12.852 WARNING __init__:185 - Caught exception while searching for autopilot interface: 'RuntimeError("Could not find Autopilot interface on DBus backend '<session bus :1.5968 /com/canonical/Autopilot/Introspection>'",)' | 171 | 12:07:51.739 INFO _launcher:567 - Killing process 31240 |
319 | 186 | 13:41:12.863 WARNING dbus:464 - Generating introspection instance for type 'Root' based on generic class. | 172 | 12:07:51.785 INFO testresult:44 - OK: autopilotqml.test_main.TestPage.test_page_title |
320 | 187 | 13:41:12.864 DEBUG dbus:338 - Selecting objects of type QMainWindow with attributes: {} | 173 | |
321 | 188 | 13:41:12.871 WARNING dbus:464 - Generating introspection instance for type 'QMainWindow' based on generic class. | 174 | Ran 1 test in 1.462s |
322 | 189 | 13:41:12.886 INFO testcase:380 - waiting for process to exit. | 175 | OK |
303 | 190 | 13:41:13.983 INFO testresult:35 - OK: example.tests.test_window.MainWindowTitleTests.test_main_window_title_string | ||
304 | 191 | |||
305 | 192 | Ran 1 test in 2.370s | ||
306 | 193 | OK | ||
323 | 194 | 176 | ||
324 | 195 | You may also specify '-v' twice for even more output (this is rarely useful for test authors however). | 177 | You may also specify '-v' twice for even more output (this is rarely useful for test authors however). |
325 | 196 | 178 | ||
329 | 197 | Both the 'list' and 'run' commands take a test id as an argument. You may be as generic, or as specific as you like. In the examples above, we will list and run all tests in the 'example' package (i.e.- all tests), but we could specify a more specific run criteria if we only wanted to run some of the tests. For example, to only run the single test we've written, we can execute:: | 179 | Both the 'list' and 'run' commands take a test id as an argument. You may be as generic, or as specific as you like. In the examples above, we will list and run all tests in the 'autopilotqml' package (i.e.- all tests), but we could specify a more specific run criteria if we only wanted to run some of the tests. For example, to only run the single test we've written, we can execute:: |
330 | 198 | 180 | ||
331 | 199 | $ autopilot3 run example.tests.test_window.MainWindowTitleTests.test_main_window_title_string | 181 | $ autopilot3 run autopilotqml.test_main.TestPage.test_page_title |
332 | 182 | |||
333 | 183 | or run just the tests for TestPage:: | ||
334 | 184 | |||
335 | 185 | $ autopilot3 run autopilotqml.test_main.TestPage.test_page_title | ||
336 | 186 | |||
337 | 187 | or run all the tests inside test_main:: | ||
338 | 188 | |||
339 | 189 | $ autopilot3 run autopilotqml.test_main | ||
340 | 200 | 190 | ||
341 | 201 | .. _tut_test_with_interaction: | 191 | .. _tut_test_with_interaction: |
342 | 202 | 192 | ||
343 | 203 | A Test with Interaction | 193 | A Test with Interaction |
344 | 204 | ======================= | 194 | ======================= |
345 | 205 | 195 | ||
388 | 206 | Now lets take a look at some simple tests with some user interaction. First, update the test application with some input and output controls:: | 196 | Now lets take a look at some simple tests with some user interaction. First, update the test application with some buttons: |
389 | 207 | 197 | ||
390 | 208 | #!/usr/bin/env python | 198 | |
349 | 209 | # File: testapp.py | ||
350 | 210 | |||
351 | 211 | from PyQt4 import QtGui | ||
352 | 212 | from sys import argv | ||
353 | 213 | |||
354 | 214 | class AutopilotHelloWorld(QtGui.QWidget): | ||
355 | 215 | def __init__(self): | ||
356 | 216 | super(AutopilotHelloWorld, self).__init__() | ||
357 | 217 | |||
358 | 218 | self.hello = QtGui.QPushButton("Hello") | ||
359 | 219 | self.hello.clicked.connect(self.say_hello) | ||
360 | 220 | |||
361 | 221 | self.goodbye = QtGui.QPushButton("Goodbye") | ||
362 | 222 | self.goodbye.clicked.connect(self.say_goodbye) | ||
363 | 223 | |||
364 | 224 | self.response = QtGui.QLabel("Response: None") | ||
365 | 225 | |||
366 | 226 | grid = QtGui.QGridLayout() | ||
367 | 227 | grid.addWidget(self.hello, 0, 0) | ||
368 | 228 | grid.addWidget(self.goodbye, 0, 1) | ||
369 | 229 | grid.addWidget(self.response, 1, 0, 1, 2) | ||
370 | 230 | self.setLayout(grid) | ||
371 | 231 | self.show() | ||
372 | 232 | self.setWindowTitle("Hello World") | ||
373 | 233 | |||
374 | 234 | def say_hello(self): | ||
375 | 235 | self.response.setText('Response: Hello') | ||
376 | 236 | |||
377 | 237 | def say_goodbye(self): | ||
378 | 238 | self.response.setText('Response: Goodbye') | ||
379 | 239 | |||
380 | 240 | |||
381 | 241 | def main(): | ||
382 | 242 | app = QtGui.QApplication(argv) | ||
383 | 243 | ahw = AutopilotHelloWorld() | ||
384 | 244 | app.exec_() | ||
385 | 245 | |||
386 | 246 | if __name__ == '__main__': | ||
387 | 247 | main() | ||
391 | 248 | 199 | ||
392 | 249 | We've reorganized the application code into a class to make the event handling easier. Then we added two input controls, the ``hello`` and ``goodbye`` buttons and an output control, the ``response`` label. | 200 | We've reorganized the application code into a class to make the event handling easier. Then we added two input controls, the ``hello`` and ``goodbye`` buttons and an output control, the ``response`` label. |
393 | 250 | 201 | ||
394 | @@ -252,65 +203,65 @@ | |||
395 | 252 | 203 | ||
396 | 253 | Since we're adding a new category of tests, button response tests, we should organize them into a new class. Our tests module now looks like:: | 204 | Since we're adding a new category of tests, button response tests, we should organize them into a new class. Our tests module now looks like:: |
397 | 254 | 205 | ||
451 | 255 | from autopilot.testcase import AutopilotTestCase | 206 | from autopilot.testcase import AutopilotTestCase |
452 | 256 | from os.path import abspath, dirname, join | 207 | from os.path import abspath, dirname, join |
453 | 257 | from testtools.matchers import Equals | 208 | from testtools.matchers import Equals |
454 | 258 | 209 | ||
455 | 259 | from autopilot.input import Mouse | 210 | from autopilot.input import Mouse |
456 | 260 | from autopilot.matchers import Eventually | 211 | from autopilot.matchers import Eventually |
457 | 261 | 212 | ||
458 | 262 | class HelloWorldTestBase(AutopilotTestCase): | 213 | class HelloWorldTestBase(AutopilotTestCase): |
459 | 263 | 214 | ||
460 | 264 | def launch_application(self): | 215 | def launch_application(self): |
461 | 265 | """Work out the full path to the application and launch it. | 216 | """Work out the full path to the application and launch it. |
462 | 266 | 217 | ||
463 | 267 | This is necessary since our test application will not be in $PATH. | 218 | This is necessary since our test application will not be in $PATH. |
464 | 268 | 219 | ||
465 | 269 | :returns: The application proxy object. | 220 | :returns: The application proxy object. |
466 | 270 | 221 | ||
467 | 271 | """ | 222 | """ |
468 | 272 | full_path = abspath(join(dirname(__file__), '..', '..', 'testapp.py')) | 223 | full_path = abspath(join(dirname(__file__), '..', '..', 'testapp.py')) |
469 | 273 | return self.launch_test_application(full_path, app_type='qt') | 224 | return self.launch_test_application(full_path, app_type='qt') |
470 | 274 | 225 | ||
471 | 275 | 226 | ||
472 | 276 | class MainWindowTitleTests(HelloWorldTestBase): | 227 | class MainWindowTitleTests(HelloWorldTestBase): |
473 | 277 | 228 | ||
474 | 278 | def test_main_window_title_string(self): | 229 | def test_main_window_title_string(self): |
475 | 279 | """The main window title must be 'Hello World'.""" | 230 | """The main window title must be 'Hello World'.""" |
476 | 280 | app_root = self.launch_application() | 231 | app_root = self.launch_application() |
477 | 281 | main_window = app_root.select_single('AutopilotHelloWorld') | 232 | main_window = app_root.select_single('AutopilotHelloWorld') |
478 | 282 | 233 | ||
479 | 283 | self.assertThat(main_window.windowTitle, Equals("Hello World")) | 234 | self.assertThat(main_window.windowTitle, Equals("Hello World")) |
480 | 284 | 235 | ||
481 | 285 | 236 | ||
482 | 286 | class ButtonResponseTests(HelloWorldTestBase): | 237 | class ButtonResponseTests(HelloWorldTestBase): |
483 | 287 | 238 | ||
484 | 288 | def test_hello_response(self): | 239 | def test_hello_response(self): |
485 | 289 | """The response text must be 'Response: Hello' after a Hello click.""" | 240 | """The response text must be 'Response: Hello' after a Hello click.""" |
486 | 290 | app_root = self.launch_application() | 241 | app_root = self.launch_application() |
487 | 291 | response = app_root.select_single('QLabel') | 242 | response = app_root.select_single('QLabel') |
488 | 292 | hello = app_root.select_single('QPushButton', text='Hello') | 243 | hello = app_root.select_single('QPushButton', text='Hello') |
489 | 293 | 244 | ||
490 | 294 | self.mouse.click_object(hello) | 245 | self.mouse.click_object(hello) |
491 | 295 | 246 | ||
492 | 296 | self.assertThat(response.text, Eventually(Equals('Response: Hello'))) | 247 | self.assertThat(response.text, Eventually(Equals('Response: Hello'))) |
493 | 297 | 248 | ||
494 | 298 | def test_goodbye_response(self): | 249 | def test_goodbye_response(self): |
495 | 299 | """The response text must be 'Response: Goodbye' after a Goodbye | 250 | """The response text must be 'Response: Goodbye' after a Goodbye |
496 | 300 | click.""" | 251 | click.""" |
497 | 301 | app_root = self.launch_application() | 252 | app_root = self.launch_application() |
498 | 302 | response = app_root.select_single('QLabel') | 253 | response = app_root.select_single('QLabel') |
499 | 303 | goodbye = app_root.select_single('QPushButton', text='Goodbye') | 254 | goodbye = app_root.select_single('QPushButton', text='Goodbye') |
500 | 304 | 255 | ||
501 | 305 | self.mouse.click_object(goodbye) | 256 | self.mouse.click_object(goodbye) |
502 | 306 | 257 | ||
503 | 307 | self.assertThat(response.text, Eventually(Equals('Response: Goodbye'))) | 258 | self.assertThat(response.text, Eventually(Equals('Response: Goodbye'))) |
504 | 308 | 259 | ||
505 | 309 | In addition to the new class, ``ButtonResponseTests``, you'll notice a few other changes. First, two new import lines were added to support the new tests. Next, the existing ``MainWindowTitleTests`` class was refactored to subclass from a base class, ``HelloWorldTestBase``. The base class contains the ``launch_application`` method which is used for all test cases. Finally, the object type of the main window changed from ``QMainWindow`` to ``AutopilotHelloWorld``. The change in object type is a result of our test application being refactored into a class called ``AutopilotHelloWorld``. | 260 | In addition to the new class, ``ButtonResponseTests``, you'll notice a few other changes. First, two new import lines were added to support the new tests. Next, the existing ``MainWindowTitleTests`` class was refactored to subclass from a base class, ``HelloWorldTestBase``. The base class contains the ``launch_application`` method which is used for all test cases. Finally, the object type of the main window changed from ``QMainWindow`` to ``AutopilotHelloWorld``. The change in object type is a result of our test application being refactored into a class called ``AutopilotHelloWorld``. |
506 | 310 | 261 | ||
507 | 311 | .. otto:: **Be careful when identifing user interface controls** | 262 | .. otto:: **Be careful when identifing user interface controls** |
508 | 312 | 263 | ||
510 | 313 | Notice that our simple refactoring of the test application forced a change to the test for the main window. When developing application code, put a little extra thought into how the user interface controls will be identified in the tests. Identify objects with attributes that are likely to remain constant as the application code is developed. | 264 | Notice that our simple refactoring of the test application forced a change to the test for the main window. When developing application code, put a little extra thought into how the user interface controls will be identified in the tests. Identify objects with attributes that are likely to remain constant as the application code is developed. |
511 | 314 | 265 | ||
512 | 315 | The ``ButtonResponseTests`` class adds two new tests, one for each input control. Each test identifies the user interface controls that need to be used, performs a single, specific action, and then verifies the outcome. In ``test_hello_response``, we first identify the ``QLabel`` control which contains the output we need to check. We then identify the ``Hello`` button. As the application has two ``QPushButton`` controls, we must further refine the ``select_single`` call by specifing an additional property. In this case, we use the button text. Next, an input action is triggered by instructing the ``mouse`` to click the ``Hello`` button. Finally, the test asserts that the response label text matches the expected string. The second test repeats the same process with the ``Goodbye`` button. | 266 | The ``ButtonResponseTests`` class adds two new tests, one for each input control. Each test identifies the user interface controls that need to be used, performs a single, specific action, and then verifies the outcome. In ``test_hello_response``, we first identify the ``QLabel`` control which contains the output we need to check. We then identify the ``Hello`` button. As the application has two ``QPushButton`` controls, we must further refine the ``select_single`` call by specifing an additional property. In this case, we use the button text. Next, an input action is triggered by instructing the ``mouse`` to click the ``Hello`` button. Finally, the test asserts that the response label text matches the expected string. The second test repeats the same process with the ``Goodbye`` button. |
513 | 316 | 267 | ||
514 | @@ -321,6 +272,6 @@ | |||
515 | 321 | 272 | ||
516 | 322 | .. otto:: **Use Eventually when asserting any user interface condition** | 273 | .. otto:: **Use Eventually when asserting any user interface condition** |
517 | 323 | 274 | ||
519 | 324 | You may find that when running tests, the application is often ready with the outcome by the time autopilot is able to test the assertion without using :class:`~autopilot.matchers.Eventually`. However, this may not always be true when running your test suite on different hardware. | 275 | You may find that when running tests, the application is often ready with the outcome by the time autopilot is able to test the assertion without using :class:`~autopilot.matchers.Eventually`. However, this may not always be true when running your test suite on different hardware. |
520 | 325 | 276 | ||
521 | 326 | .. TODO: Continue to discuss the issues with running tests & application in separate processes, and how the Eventually matcher helps us overcome these problems. Cover the various ways the matcher can be used. | 277 | .. TODO: Continue to discuss the issues with running tests & application in separate processes, and how the Eventually matcher helps us overcome these problems. Cover the various ways the matcher can be used. |
522 | 327 | 278 | ||
523 | === modified file 'docs/tutorial/what_is_autopilot.rst' | |||
524 | --- docs/tutorial/what_is_autopilot.rst 2014-05-15 00:45:08 +0000 | |||
525 | +++ docs/tutorial/what_is_autopilot.rst 2015-01-27 20:14:32 +0000 | |||
526 | @@ -11,7 +11,7 @@ | |||
527 | 11 | Where is Autopilot used? | 11 | Where is Autopilot used? |
528 | 12 | ######################## | 12 | ######################## |
529 | 13 | 13 | ||
531 | 14 | Autopilot was designed to test the `Unity 3D <http://unity.ubuntu.com/>`_ shell. However, since then it has been used to test a number of other applications, including: | 14 | Autopilot was originally designed to test the `Unity 3D <http://unity.ubuntu.com/>`_ shell. However, since then it has been used to test a number of other applications, including: |
532 | 15 | 15 | ||
533 | 16 | * Core Ubuntu GUI applications. | 16 | * Core Ubuntu GUI applications. |
534 | 17 | * Mobile phone applications for the Ubuntu Phone & Ubuntu Tablet. | 17 | * Mobile phone applications for the Ubuntu Phone & Ubuntu Tablet. |