# -*- 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/ #