1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
|
# -*- coding: utf-8 -*-
# **qunittest** is an standard Python `unittest` enhancement for PyQt4,
# allowing
# you to test asynchronous code using standard synchronous testing facility.
#
# The source for `qunittest` is available on [GitHub][gh], and released under
# the MIT license.
#
# Slightly modified by The Leap Project.
### Prerequisites
# Import unittest2 or unittest
try:
import unittest2 as unittest
except ImportError:
import unittest
# ... and some standard Python libraries
import sys
import functools
import contextlib
import re
# ... and several PyQt classes
from PyQt4.QtCore import QTimer
from PyQt4.QtTest import QTest
from PyQt4 import QtGui
### The code
# Override standard main method, by invoking it inside PyQt event loop
def main(*args, **kwargs):
qapplication = QtGui.QApplication(sys.argv)
QTimer.singleShot(0, unittest.main(*args, **kwargs))
qapplication.exec_()
"""
This main substitute does not integrate with unittest.
Note about mixing the event loop and unittests:
Unittest will fail if we keep more than one reference to a QApplication.
(pyqt expects to be and only one).
So, for the things that need a QApplication to exist, do something like:
self.app = QApplication()
QtGui.qApp = self.app
in the class setUp, and::
QtGui.qApp = None
self.app = None
in the class tearDown.
For some explanation about this, see
http://stuvel.eu/blog/127/multiple-instances-of-qapplication-in-one-process
and
http://www.riverbankcomputing.com/pipermail/pyqt/2010-September/027705.html
"""
# Helper returning the name of a given signal
def _signal_name(signal):
s = repr(signal)
name_re = "signal (\w+) of (\w+)"
match = re.search(name_re, s, re.I)
if not match:
return "??"
return "%s#%s" % (match.group(2), match.group(1))
class _SignalConnector(object):
""" Encapsulates signal assertion testing """
def __init__(self, test, signal, callable_):
self.test = test
self.callable_ = callable_
self.called_with = None
self.emited = False
self.signal = signal
self._asserted = False
signal.connect(self.on_signal_emited)
# Store given parameters and mark signal as `emited`
def on_signal_emited(self, *args, **kwargs):
self.called_with = (args, kwargs)
self.emited = True
def assertEmission(self):
# Assert once wheter signal was emited or not
was_asserted = self._asserted
self._asserted = True
if not was_asserted:
if not self.emited:
self.test.fail(
"signal %s not emited" % (_signal_name(self.signal)))
# Call given callable is necessary
if self.callable_:
args, kwargs = self.called_with
self.callable_(*args, **kwargs)
def __enter__(self):
# Assert emission when context is entered
self.assertEmission()
return self.called_with
def __exit__(self, *_):
return False
### Unit Testing
# `qunittest` does not force much abould how test should look - it just adds
# several helpers for asynchronous code testing.
#
# Common test case may look like this:
#
# import qunittest
# from calculator import Calculator
#
# class TestCalculator(qunittest.TestCase):
# def setUp(self):
# self.calc = Calculator()
#
# def test_should_add_two_numbers_synchronously(self):
# # given
# a, b = 2, 3
#
# # when
# r = self.calc.add(a, b)
#
# # then
# self.assertEqual(5, r)
#
# def test_should_calculate_factorial_in_background(self):
# # given
#
# # when
# self.calc.factorial(20)
#
# # then
# self.assertEmited(self.calc.done) with (args, kwargs):
# self.assertEqual([2432902008176640000], args)
#
# if __name__ == "__main__":
# main()
#
# Test can be run by typing:
#
# python test_calculator.py
#
# Automatic test discovery is not supported now, because testing PyQt needs
# an instance of `QApplication` and its `exec_` method is blocking.
#
### TestCase class
class TestCase(unittest.TestCase):
"""
Extends standard `unittest.TestCase` with several PyQt4 testing features
useful for asynchronous testing.
"""
def __init__(self, *args, **kwargs):
super(TestCase, self).__init__(*args, **kwargs)
self._clearSignalConnectors()
self._succeeded = False
self.addCleanup(self._clearSignalConnectors)
self.tearDown = self._decorateTearDown(self.tearDown)
### Protected methods
def _clearSignalConnectors(self):
self._connectedSignals = []
def _decorateTearDown(self, tearDown):
@functools.wraps(tearDown)
def decorator():
self._ensureEmitedSignals()
return tearDown()
return decorator
def _ensureEmitedSignals(self):
"""
Checks if signals were acually emited. Raises AssertionError if no.
"""
# TODO: add information about line
for signal in self._connectedSignals:
signal.assertEmission()
### Assertions
def assertEmited(self, signal, callable_=None, timeout=1):
"""
Asserts if given `signal` was emited. Waits 1 second by default,
before asserts signal emission.
If `callable_` is given, it should be a function which takes two
arguments: `args` and `kwargs`. It will be called after blocking
operation or when assertion about signal emission is made and
signal was emited.
When timeout is not `False`, method call is blocking, and ends
after `timeout` seconds. After that time, it validates wether
signal was emited.
When timeout is `False`, method is non blocking, and test should wait
for signals afterwards. Otherwise, at the end of the test, all
signal emissions are checked if appeared.
Function returns context, which yields to list of parameters given
to signal. It can be useful for testing given parameters. Following
code:
with self.assertEmited(widget.signal) as (args, kwargs):
self.assertEqual(1, len(args))
self.assertEqual("Hello World!", args[0])
will wait 1 second and test for correct parameters, is signal was
emtied.
Note that code:
with self.assertEmited(widget.signal, timeout=False) as (a, k):
# Will not be invoked
will always fail since signal cannot be emited in the time of its
connection - code inside the context will not be invoked at all.
"""
connector = _SignalConnector(self, signal, callable_)
self._connectedSignals.append(connector)
if timeout:
self.waitFor(timeout)
connector.assertEmission()
return connector
### Helper methods
@contextlib.contextmanager
def invokeAfter(self, seconds, callable_=None):
"""
Waits given amount of time and executes the context.
If `callable_` is given, executes it, instead of context.
"""
self.waitFor(seconds)
if callable_:
callable_()
else:
yield
def waitFor(self, seconds):
"""
Waits given amount of time.
self.widget.loadImage(url)
self.waitFor(seconds=10)
"""
QTest.qWait(seconds * 1000)
def succeed(self, bool_=True):
""" Marks test as suceeded for next `failAfter()` invocation. """
self._succeeded = self._succeeded or bool_
def failAfter(self, seconds, message=None):
"""
Waits given amount of time, and fails the test if `succeed(bool)`
is not called - in most common case, `succeed(bool)` should be called
asynchronously (in signal handler):
self.widget.signal.connect(lambda: self.succeed())
self.failAfter(1, "signal not emited?")
After invocation, test is no longer consider as succeeded.
"""
self.waitFor(seconds)
if not self._succeeded:
self.fail(message)
self._succeeded = False
### Credits
#
# * **Who is responsible:** [Dawid Fatyga][df]
# * **Source:** [GitHub][gh]
# * **Doc. generator:** [rocco][ro]
#
# [gh]: https://www.github.com/dejw/qunittest
# [df]: https://github.com/dejw
# [ro]: http://rtomayko.github.com/rocco/
#
|