Qt event loop and unit testing?
Solution 1
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.
Solution 2
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 QEvent
s 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>*/
}
Solution 3
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
Related videos on Youtube
TheMeaningfulEngineer
Would you like to release working software more often? Is testing the bottleneck of your develpoment process? Do you have coorfdinated beta testing in place? Are developers overwhelemed with unclear bug reports? I help companies refine and automate the testing process resulting in a sustainable delivery of quality software. Let's have a non binding virtual coffe and talk about the problems you're facing.
Updated on June 04, 2022Comments
-
TheMeaningfulEngineer almost 2 years
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?
-
vahancho about 10 yearsIn the main() function? You can use
QTEST_MAIN(CommunicationProtocolTest)
macro in your unit test. -
Kuba hasn't forgotten Monica about 10 years
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 aQAbstractSocket
, used for testing. -
TheMeaningfulEngineer about 10 years@vahancho Thanks, that solved the issue, however it limits me to test only a single class per run.
-
vahancho about 10 years@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.
-
TheMeaningfulEngineer about 10 years@vahancho I'm having one test class per one class that needs testing. For a bigger project the lack of grouping would be missed.
-
vahancho about 10 years@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.
-
TheMeaningfulEngineer about 10 yearsOne 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.
-
-
fgiraldeau almost 7 years
QSignalSpy::wait()
requiresQTEST_MAIN
instead ofQTEST_APPLESS_MAIN
, otherwise I getQEventLoop: Cannot be used without QApplication
. -
Halfgaar about 6 yearsOne may also want
QTEST_GUILESS_MAIN
, otherwise you need a display. -
user2019716 over 4 yearsworks 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.