25

I'we started experimenting with unit testing in Qt and would like to hear comments on a scenario that involves unit testing signals and slots.

Here is an example:

The code i would like to test is (m_socket is a pointer to QTcpSocket):

void CommunicationProtocol::connectToCamera()
{
    m_socket->connectToHost(m_cameraIp,m_port);
}

Since that is an asynchronous call i can't test a returned value. I would however like to test if the response signal that the socket emits on a successful connection (void connected ()) is in fact emitted.

I've written the test below:

void CommunicationProtocolTest::testConnectToCammera()
{
    QSignalSpy spy(communicationProtocol->m_socket, SIGNAL(connected()));
    communicationProtocol->connectToCamera();
    QTest::qWait(250);
    QCOMPARE(spy.count(), 1);
}

My motivation was, if the response doesn't happen in 250ms, something is wrong.

However, the signal is never caught, and I can't say for sure if it's even emitted. But I've noticed that I'm not starting the event loop anywhere in the test project. In the development project, the event loop is started in main with QCoreApplication::exec().


To sum it up, when unit testing a class that depends on signals and slots, where should the

QCoreApplication a(argc, argv);
return a.exec();

be run in the test environment?

Paul Sweatte
  • 24,148
  • 7
  • 127
  • 265
TheMeaningfulEngineer
  • 15,679
  • 27
  • 85
  • 143
  • 2
    In the main() function? You can use `QTEST_MAIN(CommunicationProtocolTest)` macro in your unit test. – vahancho Feb 06 '14 at 14:53
  • 3
    `qWait` spins its own event loop, so that's not a problem. Try with a longer timeout. Ideally, you would provide your own mockup implementation of a `QAbstractSocket`, used for testing. – Kuba hasn't forgotten Monica Feb 06 '14 at 15:11
  • @vahancho Thanks, that solved the issue, however it limits me to test only a single class per run. – TheMeaningfulEngineer Feb 07 '14 at 09:59
  • @Alan, why? You can test as many components as you need. Just add new test function to your class and perform verification. Or I misunderstood the limitation. – vahancho Feb 07 '14 at 10:03
  • @vahancho I'm having one test class per one class that needs testing. For a bigger project the lack of grouping would be missed. – TheMeaningfulEngineer Feb 07 '14 at 10:23
  • @Alan, I think it is common practice for unit tests - test class (an executable) per class. You run them with a batch script of so. However you can have tests for multiple classes in one test class too. – vahancho Feb 07 '14 at 10:26
  • One executable per class? In my case, that would result in opening a new test project for each class. From the current perspective, it seems a bit redundant. – TheMeaningfulEngineer Feb 07 '14 at 10:41

3 Answers3

15

I realize this is an old thread but as I hit it and as others will, there is no answer and the answer by peter and other comments still miss the point of using QSignalSpy.

To answer you original question about "where the QCoreApplication exec function is needed", basically the answer is, it isn't. QTest and QSignalSpy already has that built in.

What you really need to do in your test case is "run" the existing event loop.

Assuming you are using Qt 5: http://doc.qt.io/qt-5/qsignalspy.html#wait

So to modify your example to use the wait function:

void CommunicationProtocolTest::testConnectToCammera()
{
    QSignalSpy spy(communicationProtocol->m_socket, SIGNAL(connected()));
    communicationProtocol->connectToCamera();

    // wait returns true if 1 or more signals was emitted
    QCOMPARE(spy.wait(250), true);

    // You can be pedantic here and double check if you want
    QCOMPARE(spy.count(), 1);
}

That should give you the desired behaviour without having to create another event loop.

Dan Hogan
  • 2,323
  • 1
  • 16
  • 16
  • 4
    `QSignalSpy::wait()` requires `QTEST_MAIN` instead of `QTEST_APPLESS_MAIN`, otherwise I get `QEventLoop: Cannot be used without QApplication`. – fgiraldeau Jul 17 '17 at 21:06
  • 2
    One may also want `QTEST_GUILESS_MAIN`, otherwise you need a display. – Halfgaar Apr 03 '18 at 12:05
6

Good question. Main issues I've hit are (1) needing to let app do app.exec() yet still close-at-end to not block automated builds and (2) needing to ensure pending events get processed before relying on the result of signal/slot calls.

For (1), you could try commenting out the app.exec() in main(). BUT then if someone has FooWidget.exec() in their class that you're testing, it's going to block/hang. Something like this is handy to force qApp to exit:

int main(int argc, char *argv[]) {
    QApplication a( argc, argv );   

    //prevent hanging if QMenu.exec() got called
    smersh().KillAppAfterTimeout(300);

    ::testing::InitGoogleTest(&argc, argv);
    int iReturn = RUN_ALL_TESTS(); 
    qDebug()<<"rcode:"<<iReturn;

    smersh().KillAppAfterTimeout(1);
    return a.exec();
   }

struct smersh {
  bool KillAppAfterTimeout(int secs=10) const;
};

bool smersh::KillAppAfterTimeout(int secs) const {
  QScopedPointer<QTimer> timer(new QTimer);
  timer->setSingleShot(true);
  bool ok = timer->connect(timer.data(),SIGNAL(timeout()),qApp,SLOT(quit()),Qt::QueuedConnection);
  timer->start(secs * 1000); // N seconds timeout
  timer.take()->setParent(qApp);
  return ok;
}

For (2), basically you have to coerce QApplication into finishing up the queued events if you're trying to verify things like QEvents from Mouse + Keyboard have expected outcome. This FlushEvents<>() method is helpful:

template <class T=void> struct FlushEvents {     
 FlushEvents() {
 int n = 0;
 while(++n<20 &&  qApp->hasPendingEvents() ) {
   QApplication::sendPostedEvents();
   QApplication::processEvents(QEventLoop::AllEvents);
   YourThread::microsec_wait(100);
 }
 YourThread::microsec_wait(1*1000);
} };

Usage example below. "dialog" is instance of MyDialog. "baz" is instance of Baz. "dialog" has a member of type Bar. When a Bar selects a Baz, it emits a signal; "dialog" is connected to the signal and we need to make sure the associated slot has gotten the message.

void Bar::select(Baz*  baz) {
  if( baz->isValid() ) {
     m_selected << baz;
     emit SelectedBaz();//<- dialog has slot for this
}  }    

TEST(Dialog,BarBaz) {  /*<code>*/
dialog->setGeometry(1,320,400,300); 
dialog->repaint();
FlushEvents<>(); // see it on screen (for debugging)

//set state of dialog that has a stacked widget
dialog->setCurrentPage(i);
qDebug()<<"on page: "
        <<i;      // (we don't see it yet)
FlushEvents<>();  // Now dialog is drawn on page i 

dialog->GetBar()->select(baz); 
FlushEvents<>(); // *** without this, the next test
                 //           can fail sporadically.

EXPECT_TRUE( dialog->getSelected_Baz_instances()
                                 .contains(baz) );
/*<code>*/
}
peter karasev
  • 2,578
  • 1
  • 28
  • 38
2

I had a similar issue with Qt::QueuedConnection (event is queued automatically if the sender and the receiver belongs to different threads). Without a proper event loop in that situation, the internal state of objects depending on event processing will not be updated. To start an event loop when running QTest, change the macro QTEST_APPLESS_MAIN at the bottom of the file to QTEST_MAIN. Then, calling qApp->processEvents() will actually process events, or you can start another event loop with QEventLoop.

   QSignalSpy spy(&foo, SIGNAL(ready()));
   connect(&foo, SIGNAL(ready()), &bar, SLOT(work()), Qt::QueuedConnection);
   foo.emitReady();
   QCOMPARE(spy.count(), 1);        // QSignalSpy uses Qt::DirectConnection
   QCOMPARE(bar.received, false);   // bar did not receive the signal, but that is normal: there is no active event loop
   qApp->processEvents();           // Manually trigger event processing ...
   QCOMPARE(bar.received, true);    // bar receives the signal only if QTEST_MAIN() is used
fgiraldeau
  • 2,384
  • 1
  • 19
  • 12
  • works for me. I am testing a thread and it seems that event loop is not processing the events as you mentioned and I need to use you method with a dirty loop as follow. while(i < 100 && result == 0){ thread().msleep(100); qApp->processEvents(); } result is set to 1 when the desired slot is called. – user2019716 Sep 23 '19 at 16:03