Qt事件循环和单元测试?

26

我开始在Qt中尝试单元测试,并希望听取有关涉及单元测试信号和槽的情景的评论。

以下是一个示例:

我想要测试的代码如下(m_socket是指向QTcpSocket的指针):

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

由于这是一个异步调用,我无法测试返回值。然而,我想测试套接字在成功连接时所发出的响应信号(void connected ())是否实际发出。

我编写了以下测试:

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

我的动机是,如果响应时间超过250毫秒,则表示出现问题。

然而,该信号从未被捕获,我不能确定它是否已发出。但我注意到,在测试项目中没有启动事件循环。在开发项目中,事件循环在主函数中使用QCoreApplication::exec()启动。


总之,在对依赖于信号和槽的类进行单元测试时,应该在何处启动

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

是否在测试环境中运行?


2
在main()函数中?你可以在单元测试中使用QTEST_MAIN(CommunicationProtocolTest)宏。 - vahancho
3
qWait 会自旋一条事件循环,所以这不是问题。尝试使用更长的超时时间。最理想的情况是,您可以提供自己的 QAbstractSocket 模拟实现,用于测试。 - Kuba hasn't forgotten Monica
@vahancho 谢谢,问题已解决,但这限制了我每次只能测试一个类。 - TheMeaningfulEngineer
@Alan,为什么?你可以测试任意数量的组件。只需向你的类添加新的测试函数并执行验证即可。或者我误解了限制。 - vahancho
@Alan,我认为对于单元测试来说,每个类都有一个测试类(可执行文件)是常见的做法。你可以使用批处理脚本运行它们。但是,你也可以在一个测试类中为多个类编写测试。 - vahancho
显示剩余2条评论
3个回答

15

我知道这是一个旧的线程,但我遇到了同样的问题,其他人也会,没有答案。Peter和其他评论仍然没有理解使用QSignalSpy的要点。

回答你最初关于“何时需要QCoreApplication exec函数”的问题,基本上答案是不需要。QTest和QSignalSpy已经内置了它。

在你的测试用例中,你真正需要做的是“运行”现有的事件循环。

假设您正在使用Qt 5: http://doc.qt.io/qt-5/qsignalspy.html#wait

因此,要修改您的示例以使用等待函数:

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);
}

这样做应该可以让你达到期望的行为,而不必创建另一个事件循环。


4
如果使用 QTEST_APPLESS_MAIN 而不是 QTEST_MAIN,则 QSignalSpy::wait() 会报错 QEventLoop: Cannot be used without QApplication。因此需要使用 QTEST_MAIN - fgiraldeau
2
有时候你可能需要使用QTEST_GUILESS_MAIN,否则你需要一个显示器。 - Halfgaar

6
很好的问题。我遇到的主要问题是(1)需要让应用程序执行app.exec(),但仍然需要在结束时关闭以不阻塞自动构建,以及(2)需要确保待处理事件在依赖于信号/插槽调用结果之前得到处理。
对于(1),您可以尝试在main()中注释掉app.exec()。但是如果您正在测试的类中有FooWidget.exec(),那么它将会被阻塞/挂起。以下代码可强制qApp退出:
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;
}

对于(2),如果你想要验证鼠标和键盘等输入事件的预期结果,基本上你需要强制QApplication完成排队的事件。这个 FlushEvents<>()方法非常有用:

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);
} };

以下是使用示例。 “dialog”是MyDialog的实例。 “baz”是Baz的实例。 “dialog”有一个Bar类型的成员。 当Bar选择Baz时,它会发出信号; “dialog”连接到信号,并且我们需要确保相关的插槽已经收到了消息。

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

2

我曾遇到一个类似的问题,使用 Qt::QueuedConnection 时(当发送者和接收者位于不同的线程时,事件将自动排队)。在这种情况下,如果没有一个合适的事件循环,依赖于事件处理的对象的内部状态将不会被更新。要在运行 QTest 时启动事件循环,请将文件底部的宏 QTEST_APPLESS_MAIN 更改为 QTEST_MAIN。然后,调用 qApp->processEvents() 将实际处理事件,或者您可以使用 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

对我来说可以。我正在测试一个线程,似乎事件循环没有像你所说的那样处理事件,因此我需要使用以下带有脏循环的方法。 while(i < 100 && result == 0){ thread().msleep(100); qApp->processEvents(); } 当所需的槽被调用时,将结果设置为1。 - user2019716

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接