Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QtTest Testing Class with delayed event



  • I try to get into the unit test framework from Qt. So i thought it would be a good idea to write tests for the minesweeper clone i wrote.

    However i ran into trouble getting one test to pass:

    I wanto test in my Cell Class if the Signal flagged is emitted:

    class Cell : public QWidget
    {
        Q_OBJECT
    public:
    //....
    
        void handleMousePressEvent(QMouseEvent *event);
    
    //....
    
    signals:
        void flagged();
    //....
    };
    

    Flagged is normally emitted if the User does a right click on the widget, after it was constructed.

    I tryed to test it like this:

    void TestCell::flagged()
    {
        QFETCH(Cell::State, cellState);
        QFETCH(int, result);
    
        Cell obj{ cellState };
        QSignalSpy spy(&obj, &Cell::flagged);
    
        pressRightMouseButton(obj);
        // wait here until really counts as pressed right?
    
        QCOMPARE(spy.count(), result);
    }
    

    The strange pressRightMouseButton function does this:

    void pressRightMouseButton(Cell &cell)
    {
        QMouseEvent pressRightButton{
            QEvent::MouseButtonPress,
            cell.rect().center(),
            Qt::RightButton,
            Qt::RightButton,
            Qt::NoModifier
        };
        cell.handleMousePressEvent(&pressRightButton);
    }
    

    handleMousePressEvent is supposed to be used from a Event Filter class which gets installed in all the Cells. So i simulate its call here. This is to handle other actions like holding both mouse buttons down and slide from one widget into annother.

    However now to my main problem. The flagged signal is normally emitted like this in the Cell class:

    void Cell::handleMousePressEvent(QMouseEvent *event)
    {
        if(!(event->buttons().testFlag(Qt::LeftButton) ||
             event->buttons().testFlag(Qt::RightButton))) {
            return;
        }
        const auto elapsedTime = mElapsedTimer.restart();
    
        if(elapsedTime < QApplication::doubleClickInterval()){
            if(event->buttons().testFlag(Qt::LeftButton) &&
                    event->buttons().testFlag(Qt::RightButton)) {
    
                if(!isPressed()) {
                    pressIfReleased();
                    mNeighboursPressed = true;
                    emit pressNeighbours();
                }
            }
            for(QTimer* timer : { &mSingleMouseTimerRight,
                &mSingleMouseTimerLeft }) {
                timer->stop();
            }
            return;
        }
        if(event->buttons().testFlag(Qt::LeftButton)) {
            mSingleMouseTimerLeft.start();
        }
        else {
            mSingleMouseTimerRight.start();
        }
    }
    

    In the constructor:

    constexpr auto intervall = 50;
       for(QTimer* timer : {&mSingleMouseTimerRight, &mSingleMouseTimerLeft}){
           timer->setInterval(intervall);
           timer->setSingleShot(true);
       }
    
       connect(&mSingleMouseTimerLeft, &QTimer::timeout,
               this, &Cell::leftMouseButtonWasClicked);
       connect(&mSingleMouseTimerRight, &QTimer::timeout,
               this, &Cell::rightMouseButtonWasClicked);
    

    So if 50ms pass it counts as right of left clicked and this is called:

    void Cell::rightMouseButtonWasClicked()
    {
        mark();
    }
    
    void Cell::mark()
    {
        switch (mDisplayType) {
        case DisplayType::covered:
            mDisplayType = DisplayType::flagged;
            emit flagged();
            update();
            break;
        case DisplayType::flagged:
            if(mQuestionMarksOn) {
                mDisplayType = DisplayType::questionmark;
            }
            else {
                mDisplayType = DisplayType::covered;
            }
            emit unflagged();
            update();
            break;
        case DisplayType::questionmark:
            mDisplayType = DisplayType::covered;
            update();
            break;
        default:
            break;
        }
    }
    

    My problem is the test above seems not to pass because the 50ms delay is never reached. But how can i reach it that rightMouseButtonWasClicked is triggered?

    I tryed out QThread::msleep() but no result with that.

    If my explanation is confusing and you need more information let me know.


  • Lifetime Qt Champion

    Hi,

    Out of curiosity, why not use QTest::mouseClick ?



  • @SGaist

    But that only works if i directly override mousePressEvent in the Cell class or doesn't it?

    I cannot do that because in reality the Cell Events get managed from a CellInputHandler eventfilter Class which gets the events before to also detect stuff like move events from one to annother widget.

    That EventFilter normally calls handleMousePressEvent

    More ont this here:

    https://forum.qt.io/topic/107430/detect-if-mouse-buttons-are-pressed-when-sliding-into-widget/8

    So its then used like this:

    bool CellInputHandler::eventFilter(QObject *watched, QEvent *event)
    {
        if(event->type() == QEvent::MouseButtonPress){       
           handleMouseButtonPressEvents(watched, event);
           return true;
        }
        if(event->type() == QEvent::MouseButtonRelease){      
            handleMouseButtonReleaseEvents(watched, event);
           return true;
        }
        if(event->type() == QEvent::MouseMove) {
            handleMouseMoveEvents(event);
            return true;
        }
        return false;
    }
    
    void CellInputHandler::handleMouseButtonPressEvents(
            QObject *watched, QEvent *event)
    {
        auto mouseEvent = static_cast<QMouseEvent*>(event);
        auto cell = qobject_cast<Cell *>(watched);
        cell->handleMousePressEvent(mouseEvent);
    }
    

  • Lifetime Qt Champion

    Since you are testing a functionality that requires your custom filter, why not install it as part of your test ?
    It would match closer the real situation, no ?



  • Yes i guess you are right. I was thinking of making CellInputHandler a Singleton because the same Handler needs to be installed on all Cells. That way i could maybe install it in the constructor of Cell or is that not possible?

    Currently the parent of all Cells holds the Handler and installs it on all the Cells it owns aswell.


  • Lifetime Qt Champion

    Your current situation is cleaner. There's no need for a singleton.



  • Ok so i adjusted my tests to Test Cell with an installed CellInputHandler.

    To Check for an click and release event there is no issue:

    void TestCell::hitMine_data()
    {
        QTest::addColumn<Cell::State>("cellState");
        QTest::addColumn<int>("result");
        QTest::newRow("has mine") << Cell::State::mine << 1;
        QTest::newRow("is empty") << Cell::State::empty << 0;
    }
    
    void TestCell::hitMine()
    {
        QFETCH(Cell::State, cellState);
        QFETCH(int, result);
    
        CellInputHandler filter;
        Cell obj{ cellState };
        obj.installEventFilter(&filter);
    
        QSignalSpy spy(&obj, &Cell::hitMine);
    
        QTest::mouseClick(&obj, Qt::LeftButton, Qt::NoModifier);
    
        QCOMPARE(spy.count(), result);
    }
    

    This passes. The Widget gets clicked and released and we have the hitMinesignal emited.

    However still doing the right click does not pass:

    void TestCell::flagged_data()
    {
        QTest::addColumn<Cell::State>("cellState");
        QTest::addColumn<int>("result");
        QTest::newRow("has mine") << Cell::State::mine << 1;
        QTest::newRow("is empty") << Cell::State::empty << 1;
    }
    
    void TestCell::flagged()
    {
        QFETCH(Cell::State, cellState);
        QFETCH(int, result);
    
        CellInputHandler filter;
        Cell obj{ cellState };
        obj.installEventFilter(&filter);
    
        QSignalSpy spy(&obj, &Cell::flagged);
    
        QTest::mousePress(&obj, Qt::RightButton, Qt::NoModifier);
        QTest::mouseRelease(&obj, Qt::RightButton, Qt::NoModifier,
                            obj.rect().center(), 100);
    
        QCOMPARE(spy.count(), result);
    }
    

    here the signal gets emitted when right was clicked and 50ms are passed (See the Description in my first post).

    So what am i doing wrong here?


  • Lifetime Qt Champion

    I would go with QTest::qWaitFor.



  • This post is deleted!


  • @SGaist

    I tryed these two things but both don't work:

       QTest::mousePress(&obj, Qt::RightButton, Qt::NoModifier,
                         obj.rect().center());
    
       QTest::qWaitFor([]() { return false; }, 3000);
    
       QTest::mouseRelease(&obj, Qt::RightButton, Qt::NoModifier,
                           obj.rect().center(), 100);
    
    
       QCOMPARE(spy.count(), result);
    

    and

       QTest::mousePress(&obj, Qt::RightButton, Qt::NoModifier,
                         obj.rect().center());
    
       QTest::mouseRelease(&obj, Qt::RightButton, Qt::NoModifier,
                           obj.rect().center(), 100);
    
       QTest::qWaitFor([&]() {
           return spy.count() == 1;
       }, 3000);
    
       QCOMPARE(spy.count(), result);
    


  • i solved the issue. I found out that only when I add two press statements with a delay after the second the method works correctly.

    I fixed the bug like this:

    void Cell::handleMousePressEvent(QMouseEvent *event)
    {
        if(!(event->buttons().testFlag(Qt::LeftButton) ||
             event->buttons().testFlag(Qt::RightButton))) {
            return;
        }
    
        if(event->buttons().testFlag(Qt::LeftButton)) {
            mSingleMouseTimerLeft.start();
        }
        else if (event->buttons().testFlag(Qt::RightButton)){
            mSingleMouseTimerRight.start();
        }
    
        const auto elapsedTime = mElapsedTimer.restart();
    
        if(elapsedTime < QApplication::doubleClickInterval() && 
                event->buttons().testFlag(Qt::LeftButton) &&
                event->buttons().testFlag(Qt::RightButton)) {
    
            if(!isPressed()) {
                pressIfReleased();
                mNeighboursPressed = true;
                emit pressNeighbours();
            }
            for(QTimer* timer : { &mSingleMouseTimerRight,
                &mSingleMouseTimerLeft }) {
                timer->stop();
            }
            return;
        }
    }
    

    Before we always slided into elapsedTime < QApplication::doubleClickInterval and then the first time:

    for(QTimer* timer : { &mSingleMouseTimerRight,
              &mSingleMouseTimerLeft }) {
              timer->stop();
          }
    

    was triggered erasing the passed time. only with two nice delays i could get it to work. Now after fix it works as expected:

    void TestCell::flagged()
    {
        constexpr auto delayClickRightAndLeftTogether = 50;
        
        QFETCH(Cell::State, cellState);
        QFETCH(int, result);
    
        CellInputHandler filter;
        Cell obj{ cellState };
        obj.installEventFilter(&filter);
    
        QSignalSpy spy(&obj, &Cell::flagged);
    
        QTest::mousePress(&obj, Qt::RightButton, Qt::NoModifier,
                          obj.rect().center());
    
        spy.wait(delayClickRightAndLeftTogether);
    
        QCOMPARE(spy.count(), result);
    }
    

    So i already found a bug. Thats great.

    Note: I used spy.wait but i think its like QTest::qWaitFor Without condition right?


Log in to reply