~ubuntu-branches/ubuntu/saucy/autopilot/saucy-proposed

« back to all changes in this revision

Viewing changes to docs/tutorial/advanced_autopilot.rst

  • Committer: Package Import Robot
  • Author(s): Didier Roche
  • Date: 2013-06-07 13:33:46 UTC
  • mfrom: (57.1.1 saucy-proposed)
  • Revision ID: package-import@ubuntu.com-20130607133346-42zvbl1h2k1v54ac
Tags: 1.3daily13.06.05-0ubuntu2
autopilot-touch only suggests python-ubuntu-platform-api for now.
It's not in distro and we need that requirement to be fulfilled to
have unity 7, 100 scopes and the touch stack to distro.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
Advanced Autopilot Features
 
2
###########################
 
3
 
 
4
This document covers advanced features in autopilot.
 
5
 
 
6
.. _cleaning-up:
 
7
 
 
8
Cleaning Up
 
9
===========
 
10
 
 
11
It is vitally important that every test you run leaves the system in exactly the same state as it found it. This means that:
 
12
 
 
13
* Any files written to disk need to be removed.
 
14
* Any environment variables set during the test run need to be un-set.
 
15
* Any applications opened during the test run need to be closed again.
 
16
* Any :class:`~autopilot.input.Keyboard` keys pressed during the test need to be released again.
 
17
 
 
18
All of the methods on :class:`~autopilot.testcase.AutopilotTestCase` that alter the system state will automatically revert those changes at the end of the test. Similarly, the various input devices will release any buttons or keys that were pressed during the test. However, for all other changes, it is the responsibility of the test author to clean up those changes.
 
19
 
 
20
For example, a test might require that a file with certain content be written to disk at the start of the test. The test case might look something like this::
 
21
 
 
22
    class MyTests(AutopilotTestCase):
 
23
 
 
24
        def make_data_file(self):
 
25
            open('/tmp/datafile', 'w').write("Some data...")
 
26
 
 
27
        def test_application_opens_data_file(self):
 
28
            """Our application must be able to open a data file from disk."""
 
29
            self.make_data_file()
 
30
            # rest of the test code goes here
 
31
 
 
32
However this will leave the :file:`/tmp/datafile` on disk after the test has finished. To combat this, use the :meth:`addCleanup` method. The arguments to :meth:`addCleanup` are a callable, and then zero or more positional or keyword arguments. The Callable will be called with the positional and keyword arguments after the test has ended.
 
33
 
 
34
Cleanup actions are called in the reverse order in which they are added, and are called regardless of whether the test passed, failed, or raised an uncaught exception. To fix the above test, we might write something similar to::
 
35
 
 
36
    import os
 
37
 
 
38
 
 
39
    class MyTests(AutopilotTestCase):
 
40
 
 
41
        def make_data_file(self):
 
42
            open('/tmp/datafile', 'w').write("Some data...")
 
43
            self.addCleanup(os.remove, '/tmp/datafile')
 
44
 
 
45
        def test_application_opens_data_file(self):
 
46
            """Our application must be able to open a data file from disk."""
 
47
            self.make_data_file()
 
48
            # rest of the test code goes here
 
49
 
 
50
Note that by having the code to generate the ``/tmp/datafile`` file on disk in a separate method, the test itself can ignore the fact that these resources need to be cleaned up. This makes the tests cleaner and easier to read.
 
51
 
 
52
Test Scenarios
 
53
==============
 
54
 
 
55
Occasionally test authors will find themselves writing multiple tests that differ in one or two subtle ways. For example, imagine a hypothetical test case that tests a dictionary application. The author wants to test that certain words return no results. Without using test scenarios, there are two basic approaches to this problem. The first is to create many test cases, one for each specific scenario (*don't do this*)::
 
56
 
 
57
    class DictionaryResultsTests(AutopilotTestCase):
 
58
 
 
59
        def test_empty_string_returns_no_results(self):
 
60
            self.dictionary_app.enter_search_term("")
 
61
            self.assertThat(len(self.dictionary_app.results), Equals(0))
 
62
 
 
63
        def test_whitespace_string_returns_no_results(self):
 
64
            self.dictionary_app.enter_search_term(" \t ")
 
65
            self.assertThat(len(self.dictionary_app.results), Equals(0))
 
66
 
 
67
        def test_punctuation_string_returns_no_results(self):
 
68
            self.dictionary_app.enter_search_term(".-?<>{}[]")
 
69
            self.assertThat(len(self.dictionary_app.results), Equals(0))
 
70
 
 
71
        def test_garbage_string_returns_no_results(self):
 
72
            self.dictionary_app.enter_search_term("ljdzgfhdsgjfhdgjh")
 
73
            self.assertThat(len(self.dictionary_app.results), Equals(0))
 
74
 
 
75
The main problem here is that there's a lot of typing in order to change exactly one thing (and this hypothetical test is deliberately short, to ease clarity. Imagine a 100 line test case!). Another approach is to make the entire thing one large test (*don't do this either*)::
 
76
 
 
77
    class DictionaryResultsTests(AutopilotTestCase):
 
78
 
 
79
        def test_bad_strings_returns_no_results(self):
 
80
            bad_strings = ("",
 
81
                " \t ",
 
82
                ".-?<>{}[]",
 
83
                "ljdzgfhdsgjfhdgjh",
 
84
                )
 
85
            for input in bad_strings:
 
86
                self.dictionary_app.enter_search_term(input)
 
87
                self.assertThat(len(self.dictionary_app.results), Equals(0))
 
88
 
 
89
 
 
90
This approach makes it easier to add new input strings, but what happens when just one of the input strings stops working? It becomes very hard to find out which input string is broken, and the first string that breaks will prevent the rest of the test from running, since tests stop running when the first assertion fails.
 
91
 
 
92
The solution is to use test scenarios. A scenario is a class attribute that specifies one or more scenarios to run on each of the tests. This is best demonstrated with an example::
 
93
 
 
94
    class DictionaryResultsTests(AutopilotTestCase):
 
95
 
 
96
        scenarios = [
 
97
            ('empty string', {'input': ""}),
 
98
            ('whitespace', {'input': " \t "}),
 
99
            ('punctuation', {'input': ".-?<>{}[]"}),
 
100
            ('garbage', {'input': "ljdzgfhdsgjfhdgjh"}),
 
101
            ]
 
102
 
 
103
        def test_bad_strings_return_no_results(self):
 
104
            self.dictionary_app.enter_search_term(self.input)
 
105
            self.assertThat(len(self.dictionary_app.results), Equals(0))
 
106
 
 
107
Autopilot will run the ``test_bad_strings_return_no_results`` once for each scenario. On each test, the values from the scenario dictionary will be mapped to attributes of the test case class. In this example, that means that the 'input' dictionary item will be mapped to ``self.input``. Using scenarios has several benefits over either of the other strategies outlined above:
 
108
 
 
109
* Tests that use strategies will appear as separate tests in the test output. The test id will be the normal test id, followed by the strategy name in parenthesis. So in the example above, the list of test ids will be::
 
110
 
 
111
   DictionaryResultsTests.test_bad_strings_return_no_results(empty string)
 
112
   DictionaryResultsTests.test_bad_strings_return_no_results(whitespace)
 
113
   DictionaryResultsTests.test_bad_strings_return_no_results(punctuation)
 
114
   DictionaryResultsTests.test_bad_strings_return_no_results(garbage)
 
115
 
 
116
* Since scenarios are treated as separate tests, it's easier to debug which scenario has broken, and re-run just that one scenario.
 
117
 
 
118
* Scenarios get applied before the ``setUp`` method, which means you can use scenario values in the ``setUp`` and ``tearDown`` methods. This makes them more flexible than either of the approaches listed above.
 
119
 
 
120
.. TODO: document the use of the multiply_scenarios feature.
 
121
 
 
122
Test Logging
 
123
============
 
124
 
 
125
.. Write about how tests can write things to the log.
 
126
 
 
127
Environment Patching
 
128
====================
 
129
 
 
130
.. Document TestCase.patch_environment and it's uses.
 
131
 
 
132
Custom Assertions
 
133
=================
 
134
 
 
135
.. Document the custom assertion methods present in AutopilotTestCase
 
136
 
 
137
Platform Selection
 
138
==================
 
139
 
 
140
.. Document the methods we have to get information about the platform we're running on, and how we can skip tests based on this information.
 
141
 
 
142
Gestures and Multitouch
 
143
=======================
 
144
 
 
145
.. How do we do multi-touch & gestures?
 
146
 
 
147
.. _tut-picking-backends:
 
148
 
 
149
Advanced Backend Picking
 
150
========================
 
151
 
 
152
Several features in autopilot are provided by more than one backend. For example, the :mod:`autopilot.input` module contains the :class:`~autopilot.input.Keyboard`, :class:`~autopilot.input.Mouse` and :class:`~autopilot.input.Touch` classes, each of which can use more than one implementation depending on the platform the tests are being run on.
 
153
 
 
154
For example, when running autopilot on a traditional ubuntu desktop platform, :class:`~autopilot.input.Keyboard` input events are probably created using the X11 client libraries. On a phone platform, X11 is not present, so autopilot will instead choose to generate events using the kernel UInput device driver instead.
 
155
 
 
156
Other autopilot systems that make use of multiple backends include the :mod:`autopilot.display` and :mod:`autopilot.process` modules. Every class in these modules follows the same construction pattern:
 
157
 
 
158
Default Creation
 
159
++++++++++++++++
 
160
 
 
161
By default, calling the ``create()`` method with no arguments will return an instance of the class that is appropriate to the current platform. For example::
 
162
    >>> from autopilot.input import Keyboard
 
163
    >>> kbd = Keyboard.create()
 
164
 
 
165
The code snippet above will create an instance of the Keyboard class that uses X11 on Desktop systems, and UInput on other systems. On the rare occaison when test authors need to construct these objects themselves, we expect that the default creation pattern to be used.
 
166
 
 
167
Picking a Backend
 
168
+++++++++++++++++
 
169
 
 
170
Test authors may sometimes want to pick a specific backend. The possible backends are documented in the API documentation for each class. For example, the documentation for the :meth:`autopilot.input.Keyboard.create` method says there are two backends available: the ``X11`` backend, and the ``UInput`` backend. These backends can be specified in the create method. For example, to specify that you want a Keyboard that uses X11 to generate it's input events::
 
171
 
 
172
    >>> from autopilot.input import Keyboard
 
173
    >>> kbd = Keyboard.create("X11")
 
174
 
 
175
Similarly, to specify that a UInput keyboard should be created::
 
176
 
 
177
    >>> from autopilot.input import Keyboard
 
178
    >>> kbd = Keyboard.create("UInput")
 
179
 
 
180
.. warning:: Care must be taken when specifying specific backends. There is no guarantee that the backend you ask for is going to be available across all platforms. For that reason, using the default creation method is encouraged.
 
181
 
 
182
Possible Errors when Creating Backends
 
183
++++++++++++++++++++++++++++++++++++++
 
184
 
 
185
Lots of things can go wrong when creating backends with the ``create`` method.
 
186
 
 
187
If autopilot is unable to create any backends for your current platform, a :exc:`RuntimeError` exception will be raised. It's ``message`` attribute will contain the error message from each backend that autopilot tried to create.
 
188
 
 
189
If a preferred backend was specified, but that backend doesn't exist (probably the test author mis-spelled it), a :exc:`RuntimeError` will be raised::
 
190
 
 
191
    >>> from autopilot.input import Keyboard
 
192
    >>> try:
 
193
    ...     kbd = Keyboard.create("uinput")
 
194
    ... except RuntimeError as e:
 
195
    ...     print "Unable to create keyboard:", e
 
196
    ...
 
197
    Unable to create keyboard: Unknown backend 'uinput'
 
198
 
 
199
In this example, ``uinput`` was mis-spelled (backend names are case sensitive). Specifying the correct backend name works as expected::
 
200
 
 
201
    >>> from autopilot.input import Keyboard
 
202
    >>> kbd = Keyboard.create("UInput")
 
203
 
 
204
Finally, if the test author specifies a preferred backend, but that backend could not be created, a :exc:`autopilot.BackendException` will be raised. This is an important distinction to understand: While calling ``create()`` with no arguments will try more than one backend, specifying a backend to create will only try and create that one backend type. The BackendException instance will contain the original exception raised by the backed in it's ``original_exception`` attribute. In this example, we try and create a UInput keyboard, which fails because we don't have the correct permissions (this is something that autopilot usually handles for you)::
 
205
 
 
206
    >>> from autopilot.input import Keyboard
 
207
    >>> from autopilot import BackendException
 
208
    >>> try:
 
209
    ...     kbd = Keyboard.create("UInput")
 
210
    ... except BackendException as e:
 
211
    ...     repr(e.original_exception)
 
212
    ...     repr(e)
 
213
    ...
 
214
    'UInputError(\'"/dev/uinput" cannot be opened for writing\',)'
 
215
    'BackendException(\'Error while initialising backend. Original exception was: "/dev/uinput" cannot be opened for writing\',)'
 
216
 
 
217
 
 
218
 
 
219
Process Control
 
220
===============
 
221
 
 
222
.. Document the process stack.
 
223
 
 
224
Display Information
 
225
===================
 
226
 
 
227
.. Document the display stack.
 
228
 
 
229
.. _custom_emulators:
 
230
 
 
231
Writing Custom Emulators
 
232
========================
 
233
 
 
234
By default, autopilot will generate an object for every introspectable item in your application under test. These are generated on the fly, and derive from
 
235
:class:`~autopilot.introspection.DBusIntrospectionObject`. This gives you the usual methods of selecting other nodes in the object tree, as well the the means to inspect all the properties in that class.
 
236
 
 
237
However, sometimes you want to customize the class used to create these objects. The most common reason to want to do this is to provide methods that make it easier to inspect these objects. Autopilot allows test authors to provide their own custom classes, through a couple of simple steps:
 
238
 
 
239
1. First, you must define your own base class, to be used by all emulators in your test suite. This base class can be empty, but must derive from :class:`~autopilot.introspection.CustomEmulatorBase`. An example class might look like this::
 
240
 
 
241
    from autopilot.introspection import CustomEmulatorBase
 
242
 
 
243
 
 
244
    class EmulatorBase(CustomEmulatorBase):
 
245
        """A base class for all emulators within this test suite."""
 
246
 
 
247
2. Define the classes you want autopilot to use, instead of the default. The class name must be the same as the type you wish to override. For example, if you want to define your own custom class to be used every time autopilot generates an instance of a 'QLabel' object, the class definition would look like this::
 
248
 
 
249
    class QLabel(EmulatorBase):
 
250
 
 
251
        # Add custom methods here...
 
252
 
 
253
3. As long as this custom class has been seen by the python interpreter (usually by importing it somewhere within the test suite), autopilot will use it every time it needs to generate a ``QLabel`` instance. You can also pass this class to methods like :meth:`~autopilot.introspection.DBusIntrospectionObject.select_single` instead of a string. So, for example, the following is a valid way of selecting the QLabel instances in an application::
 
254
 
 
255
    # self.app is the application proxy object.
 
256
    # Get all QLabels in the applicaton:
 
257
    labels = self.app.select_many(QLabel)