Testing signals and slots

It's quite difficult to work the signals and slots mechanism into the unittest framework. This is not surprising, since signals and slots quintessentially join components together, and the unittests are meant to test each component separately.

However, you might want to test whether calling a method on a certain object causes it to emit the right signals. We need a bit of a custom framework for that purpose, a kind of signal test.

You can use the ConnectionBox from the following script for that purpose. It is a simple class, derived from QObject, which has one slot, slotSlot(), that can be connected to a signal with any number of arguments.

The arguments to the signal are stored in the ConnectionBox, so they can be checked later using the various assertion functions.

I have provided three assertion functions, one to check whether the signal did arrive (assertSignalArrived), one to check whether the number of arguments was right, (assertNumberOfArguments), and one to check the types of the arguments using the Python types (assertArgumentTypes). This provides typenames for all built-in types, but objects created from all user-defined classes (including PyQt classes), belong to the InstanceType. This means that you cannot check whether you got a QListViewItem or a QListView from a PyQt signal using this function.

It would be a nice exercise to extend this assert with checking objects using the QObject.className() method. Feel free...

#
# signals.py - unit-testing signals
#
import sys
import unittest
import types
from docviewdoc import DocviewDoc
from qt import *

class ConnectionBox(QObject):

    def __init__(self, *args):
        apply(QObject.__init__,(self,)+args)
        self.signalArrived=0
        self.args=[]

    def slotSlot(self, *args):
        self.signalArrived=1
        self.args=args

    def assertSignalArrived(self, signal=None):
        if  not self.signalArrived:
            raise AssertionError, ("signal %s did not arrive" % signal)

    def assertNumberOfArguments(self, number):
        if number <> len(self.args):
            raise AssertionError, \
                  ("Signal generated %i arguments, but %i were expected" %
                                    (len(self.args), number))

    def assertArgumentTypes(self, *args):
        if len(args) <> len(self.args):
            raise AssertionError, \
         ("Signal generated %i arguments, but %i were given to this function" %
                                 (len(self.args), len(args)))
        for i in range(len(args)):
            if type(self.args[i]) != args[i]:
                raise AssertionError, \
                      ( "Arguments don't match: %s received, should be %s." %
                                      (type(self.args[i]), args[i]))

class SignalsTestCase(unittest.TestCase):
    """This testcase tests the testing of signals
    """
    def setUp(self):
        self.doc=DocviewDoc()
        self.connectionBox=ConnectionBox()
        
    def tearaDown(self):
        self.doc.disConnect()
        self.doc=None
        self.connectionBox=None
        
    def checkSignalDoesArrive(self):
        """Check whether the sigDocModified signal arrives"""
        self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModified"),
                              self.connectionBox.slotSlot)
        self.doc.slotModify()
        self.connectionBox.assertSignalArrived("sigDocModified")

    def checkSignalDoesNotArrive(self):
        """Check whether the sigDocModifiedXXX signal does not arrive"""
        self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModifiedXXX"),
                                   self.connectionBox.slotSlot)
        self.doc.slotModify()
        try:
            self.connectionBox.assertSignalArrived("sigDocModifiedXXX")
        except AssertionError:
            pass
        else:
            fail("The signal _did_ arrive")

    def checkArgumentToSignal(self):
        """Check whether the sigDocModified signal has the right
           number of arguments
        """
        self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModified"),
                                   self.connectionBox.slotSlot)
        self.doc.slotModify()
        self.connectionBox.assertNumberOfArguments(1)

    def checkArgumentTypes(self):
        """Check whether the sigDocModified signal has the right 
           type of arguments.
        """
        self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModified"),
                                   self.connectionBox.slotSlot)
        self.doc.slotModify()
        self.connectionBox.assertArgumentTypes(types.IntType)


def suite():
    testSuite=unittest.makeSuite(SignalsTestCase, "check")
    return testSuite


def main():
    runner = unittest.TextTestRunner()
    runner.run(suite())

if __name__=="__main__":
    main()
    

Using this ConnectionBox, you can test your signals:

boud@calcifer:~/doc/pyqt/ch9 > python signals.py
Check whether the sigDocModified signal has the right number arguments ... ok
Check whether the sigDocModified signal has the right type of arguments ... ok
Check whether the sigDocModified signal arrives ... ok
Check whether the sigDocModifiedXXX signal does not arrive ... ok
------------------------------------------------------------------------------
Ran 4 tests in 0.003s

OK