Tuesday, August 31, 2010

Testing Qt slots and signals

Testing slots

Testing slots is very easy, because a slot is just a specially annotated method. You can call slots just like any other method you'd like to test, as shown below:
Example 25. QLabel test code, showing testing of a couple of slots
  1. #include
  2. #include
  3. #include
  4.  
  5. class testLabel: public QObject
  6. {
  7. Q_OBJECT
  8. private slots:
  9. void testChanges();
  10. };
  11.  
  12. void testLabel::testChanges()
  13. {
  14. QLabel label;
  15.  
  16. // setNum() is a QLabel slot, but we can just call it like any
  17. // other method.
  18. label.setNum( 3 );
  19. QCOMPARE( label.text(), QString("3") );
  20.  
  21. // clear() is also a slot.
  22. label.clear();
  23. QVERIFY( label.text().isEmpty() );
  24. }
  25.  
  26. QTEST_MAIN(testLabel)
  27. #include "tutorial5.moc"

Testing signals

Testing of signals is a little more difficult than testing of slots, however Qt offers a very useful class called QSignalSpy that helps a lot.
QSignalSpy is a class provided with Qt that allows you to record the signals that have been emitted from a particular QObject subclass object. You can then check that the right number of signals have been emitted, and that the right kind of signals were emitted. You can find more information on the QSignalSpy class in your Qt documentation.
An example of how you can use QSignalSpy to test a class that has signals is shown below.
Example 26. QCheckBox test code, showing testing of signals
  1. #include
  2. #include
  3. #include
  4.  
  5. class testCheckBox: public QObject
  6. {
  7. Q_OBJECT
  8. private slots:
  9. void testSignals();
  10. };
  11.  
  12. void testCheckBox::testSignals()
  13. {
  14. // You don't need to use an object created with "new" for
  15. // QSignalSpy, I just needed it in this case to test the emission
  16. // of a destroyed() signal.
  17. QCheckBox *xbox = new QCheckBox;
  18.  
  19. // We are going to have two signal monitoring classes in use for
  20. // this test.
  21. // The first monitors the stateChanged() signal.
  22. // Also note that QSignalSpy takes a pointer to the object.
  23. QSignalSpy stateSpy( xbox, SIGNAL( stateChanged(int) ) );
  24.  
  25. // Not strictly necessary, but I like to check that I have set up
  26. // my QSignalSpy correctly.
  27. QVERIFY( stateSpy.isValid() );
  28.  
  29. // Now we check to make sure we don't have any signals already
  30. QCOMPARE( stateSpy.count(), 0 );
  31.  
  32. // Here is a second monitoring class - this one for the
  33. // destroyed() signal.
  34. QSignalSpy destroyedSpy( xbox, SIGNAL( destroyed() ) );
  35. QVERIFY( destroyedSpy.isValid() );
  36.  
  37. // A sanity check to verify the initial state
  38. // This also shows that you can mix normal method checks with
  39. // signal checks.
  40. QCOMPARE( xbox->checkState(), Qt::Unchecked );
  41.  
  42. // Shouldn't already have any signals
  43. QCOMPARE( destroyedSpy.count(), 0 );
  44.  
  45. // If we change the state, we should get a signal.
  46. xbox->setCheckState( Qt::Checked );
  47. QCOMPARE( stateSpy.count(), 1 );
  48.  
  49. xbox->setCheckState( Qt::Unchecked );
  50. QCOMPARE( stateSpy.count(), 2 );
  51.  
  52. xbox->setCheckState( Qt::PartiallyChecked );
  53. QCOMPARE( stateSpy.count(), 3 );
  54.  
  55. // If we destroy the object, the signal should be emitted.
  56. delete xbox;
  57.  
  58. // So the count of objects should increase.
  59. QCOMPARE( destroyedSpy.count(), 1 );
  60.  
  61. // We can also review the signals that we collected
  62. // QSignalSpy is really a QList of QLists, so we take the first
  63. // list, which corresponds to the arguments for the first signal
  64. // we caught.
  65. QList<QVariant> firstSignalArgs = stateSpy.takeFirst();
  66. // stateChanged() only has one argument - an enumerated type (int)
  67. // So we take that argument from the list, and turn it into an integer.
  68. int firstSignalState = firstSignalArgs.at(0).toInt();
  69. // We can then check we got the right kind of signal.
  70. QCOMPARE( firstSignalState, static_cast<int>(Qt::Checked) );
  71.  
  72. // check the next signal - note that takeFirst() removes from the list
  73. QList<QVariant> nextSignalArgs = stateSpy.takeFirst();
  74. // this shows another way of fudging the argument types
  75. Qt::CheckState nextSignalState = (Qt::CheckState)nextSignalArgs.at(0).toInt();
  76. QCOMPARE( nextSignalState, Qt::Unchecked );
  77.  
  78. // and again for the third signal
  79. nextSignalArgs = stateSpy.takeFirst();
  80. nextSignalState = (Qt::CheckState)nextSignalArgs.at(0).toInt();
  81. QCOMPARE( nextSignalState, Qt::PartiallyChecked );
  82. }
  83.  
  84. QTEST_MAIN(testCheckBox)
  85. #include "tutorial5a.moc"
The first 11 lines are essentially unchanged from previous examples that we've seen. Line 15 creates the object that will be tested - as noted in the comments in lines 12-14, the only reason that I'm creating it with new is because I need to delete it in line 45 to cause the destroyed() signal to be emitted.
Line 20 sets up the first of our two QSignalSpy instances. The one in line 20 monitors the stateChanged(int) signal, and the one in line 29 monitors the destroyed() signal. If you get the name or signature of the signal wrong (for example, if you use stateChanged() instead of stateChanged(int)), then this will not be caught at compile time, but will result in a runtime failure. You can test if things were set up correctly using the isValid(), as shown in lines 24 and 30.
As shown in line 34, there is no reason why you cannot test normal methods, signals and slots in the same test.
Line 38 changes the state of the object under test, which is supposed to result in a stateChanged(int) signal being emitted. Line 39 checks that the number of signals increases from zero to one. Lines 40 and 41 repeat the process, and again in lines 42 and 43.
Line 45 deletes the object under test, and line 47 tests that the destroyed() signal has been emitted.
For signals that have arguments (such as our stateChanged(int) signal), you may also wish to check that the arguments were correct. You can do this by looking at the list of signal arguments. Exactly how you do this is fairly flexible, however for simple tests like the one in the example, you can manually work through the list using takeFirst() and check that each argument is correct. This is shown in line 52, 55 and 57 for the first signal. The same approach is shown in lines 59, 61 and 62 for the second signal, and the in lines 64 to 66 for the third signal. For a more complex set of tests, you may wish to apply some data driven techniques.

Dica
noframe
Note: You should be aware that, for some class implementations, you may need to return control to the event loop to have signals emitted. If you need this, try using the QTest::qWait() function.

No comments:

Post a Comment