Qt цикл событий и модульное тестирование?

Я начал экспериментировать с модульным тестированием в 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();

запускается в тестовой среде?

Ответ 1

Хороший вопрос. Основные проблемы, с которыми я столкнулся, - это (1) необходимость приложения app.exec(), но все еще закрытого конца, чтобы не блокировать автоматические сборки, и (2) нужно, чтобы ожидающие обработки событий обрабатывались, прежде чем полагаться на результат сигнала/слотов.

Для (1) вы можете попробовать прокомментировать app.exec() в main(). НО, если у кого-то есть 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 к завершению очереди в случае, если вы пытаетесь проверить такие вещи, как QEvent от Mouse + Keyboard, ожидали результата. Этот метод 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);
} };

Пример использования ниже.   "Диалог" - это пример MyDialog.   "база" - это пример База.   "dialog" имеет член типа Bar.  Когда Bar выбирает Baz, он излучает сигнал; "Диалог" подключен к сигналу, и нам нужно  убедитесь, что соответствующий слот получил сообщение.

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

Я понимаю, что это старый поток, но когда я его ударил, и, как и другие, ответа нет, а ответ peter и другие комментарии по-прежнему не совпадают с точкой использования QSignalSpy.

Чтобы ответить на ваш оригинальный вопрос о том, "где нужна функция exec QCoreApplication", в основном ответ есть, это не так. QTest и QSignalSpy уже имеют встроенную систему.

Что вам действительно нужно сделать в тестовом случае, это "запустить" существующий цикл событий.

Предполагая, что вы используете Qt 5: http://doc.qt.io/qt-5/qsignalspy.html#wait

Итак, чтобы изменить ваш пример, чтобы использовать функцию 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);
}

Это должно дать вам желаемое поведение без необходимости создавать другой цикл событий.

Ответ 3

У меня была аналогичная проблема с 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