206
206
A Test with Interaction
207
207
=======================
209
.. TODO: Add a second test, one that adds some keyboard / mouse interaction.
209
Now lets take a look at some simple tests with some user interaction. First, update the test application with some input and output controls::
211
#!/usr/bin/env python
213
from PyQt4 import QtGui
216
class AutopilotHelloWorld(QtGui.QWidget):
218
super(AutopilotHelloWorld, self).__init__()
220
self.hello = QtGui.QPushButton("Hello")
221
self.hello.clicked.connect(self.say_hello)
223
self.goodbye = QtGui.QPushButton("Goodbye")
224
self.goodbye.clicked.connect(self.say_goodbye)
226
self.response = QtGui.QLabel("Response: None")
228
grid = QtGui.QGridLayout()
229
grid.addWidget(self.hello, 0, 0)
230
grid.addWidget(self.goodbye, 0, 1)
231
grid.addWidget(self.response, 1, 0, 1, 2)
234
self.setWindowTitle("Hello World")
237
self.response.setText('Response: Hello')
239
def say_goodbye(self):
240
self.response.setText('Response: Goodbye')
244
app = QtGui.QApplication(argv)
245
ahw = AutopilotHelloWorld()
248
if __name__ == '__main__':
251
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.
253
The operation of the application is still very trivial, but now we can test that it actually does something in response to user input. Clicking either of the two buttons will cause the response text to change. Clicking the ``Hello`` button should result in ``Response: Hello`` while clicking the ``Goodbye`` button should result in ``Response: Goodbye``.
255
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::
257
from autopilot.testcase import AutopilotTestCase
258
from os.path import abspath, dirname, join
259
from testtools.matchers import Equals
261
from autopilot.input import Mouse
262
from autopilot.matchers import Eventually
264
class HelloWorldTestBase(AutopilotTestCase):
266
def launch_application(self):
267
"""Work out the full path to the application and launch it.
269
This is necessary since our test application will not be in $PATH.
271
:returns: The application proxy object.
274
full_path = abspath(join(dirname(__file__), '..', '..', 'testapp.py'))
275
return self.launch_test_application(full_path, app_type='qt')
278
class MainWindowTitleTests(HelloWorldTestBase):
280
def test_main_window_title_string(self):
281
"""The main window title must be 'Hello World'."""
282
app_root = self.launch_application()
283
main_window = app_root.select_single('AutopilotHelloWorld')
285
self.assertThat(main_window.windowTitle, Equals("Hello World"))
288
class ButtonResponseTests(HelloWorldTestBase):
290
def test_hello_response(self):
291
"""The response text must be 'Response: Hello' after a Hello click."""
292
app_root = self.launch_application()
293
response = app_root.select_single('QLabel')
294
hello = app_root.select_single('QPushButton', text='Hello')
296
self.mouse.click_object(hello)
298
self.assertThat(response.text, Eventually(Equals('Response: Hello')))
300
def test_goodbye_response(self):
301
"""The response text must be 'Response: Goodbye' after a Goodbye
303
app_root = self.launch_application()
304
response = app_root.select_single('QLabel')
305
goodbye = app_root.select_single('QPushButton', text='Goodbye')
307
self.mouse.click_object(goodbye)
309
self.assertThat(response.text, Eventually(Equals('Response: Goodbye')))
311
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``.
313
.. otto:: **Be careful when identifing user interface controls**
315
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.
317
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.
211
319
The Eventually Matcher
212
320
======================
214
.. TODO: 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.
322
Notice that in the ButtonResponseTests tests above, the autopilot method :class:`~autopilot.matchers.Eventually` is used in the assertion. This allows the assertion to be retried continuously until it either becomes true, or times out (the default timout is 10 seconds). This is necessary because the application and the autopilot tests run in different processes. Autopilot could test the assert before the application has completed its action. Using :class:`~autopilot.matchers.Eventually` allows the application to complete its action without having to explicitly add delays to the tests.
324
.. otto:: **Use Eventually when asserting any user interface condition**
326
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.
328
.. 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.