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

C++ integration tests with embedded QtQuick components



  • I have a QWidgets app with a few embedded QtQuick forms, and want to write integration tests that include the QtQuick components. Has anyone done this?

    I am using PyQt and pytest, but I am proficient in C++ and the QtTest framework.

    Thanks!


  • Lifetime Qt Champion

    Hi,

    Are you already using pytest-qt ?



  • @SGaist yep. And it’s helpful for QtWidget tests. But that doesn’t have any way to simulate GUI events in QML so far as I know


  • Lifetime Qt Champion

    I haven't used it yet with a QtQuick application. I think you should contact the plugin authors for more information about that use case.



  • @SGaist Well, I think an even better question might be how to do this between C++ and Qml. The pytest-qt plugin is simple enough, but would rely on the basic functionality that may or may not exist at the C++ level. So I think that is really what my question is aimed at here.


  • Lifetime Qt Champion

    Good question and I don't currently know.

    It's likely going to involve the TestCase QML type somehow.



  • I sorted out a way to do this. I write a proper C++ (or python in my case) test and then pass input events to Qml. It's quite straightforward to roll your own keyboard/mouse simulation and property verification tools.

    The steps are:

    • Obtain the QQuickItem from C++ using the objectName property with qquickWidget->rootObject()->findChild<QQuickItem>(objectName)
    • Simulate the input event on the enclosing QQuickWidget using QTest.keyClicks, QTest::mouseClick(), etc.
      • For keyboard events you have to manually focus the QQuickItem using either QQuickItem::forceActiveFocus() or with a mouse click.
      • For mouse events you have to do the math to map item coordinates to scene (i.e. QQuickWidget) coordinates. Not hard.

    Controls in qml need to have the objectName property set to a unique string for that component for the C++ test code to find them. This is redundant if you are also effectively setting the same token on the id property, but I don't know how to find a child QQuickItem by id and actually think it isn't possible:

    TextEdit {
        id: descriptionEdit
        objectName: "descriptionEdit"
    }
    

    The following is my helper methods I added to all of my QWidget classes that contain a QQuickWidget as the self.qml property, You'll notice one custom input method for selecting a particular tab in a TabBar. You could write custom methods for many other Item subclasses, such as ListView & TableView (I will do so soon).

        ## Tests                                                                                                                                                                                                     
    
        def findItem(self, objectName):
            parts = objectName.split('.')
            item = self.qml.rootObject()
            for partName in parts:
                item = item.findChild(QQuickItem, partName)
            if not item:
                raise RuntimeError('Could not find item:', objectName)
            return item
    
        def focusItem(self, objectName):
            item = self.findItem(objectName)
            item.forceActiveFocus()
            if not item.hasActiveFocus():
                raise RuntimeError('Could not set active focus on:', objectName)
            return item
    
        def keyClick(self, objectName, key):
            self.focusItem(objectName)
            util.qtbot.keyClick(self.qml, key)
    
        def keyClicks(self, objectName, s):
            self.focusItem(objectName)
            util.qtbot.keyClicks(self.qml, s)
            self.qml.rootObject().forceActiveFocus()
            if not self.qml.rootObject().hasActiveFocus():
                raise RuntimeError('Could not set active focus on root object.')
    
        def mouseClick(self, objectName, buttons):
            item = self.findItem(objectName)
            rect = item.mapRectToScene(QRectF(0, 0, item.width(), item.height())).toRect()
            util.qtbot.mouseClick(self.qml, buttons, Qt.NoModifier, rect.center())
    
        def clickTabWidgetPage(self, objectName, iTab):
            item = self.findItem(objectName)
            rect = item.mapRectToScene(QRectF(0, 0, item.width(), item.height())).toRect()
            count = item.property('count')
            tabWidth = rect.width() / count
            tabStartX = tabWidth * iTab
            tabEndX = tabWidth * iTab + tabWidth
            tabCenterX = tabStartX + ((tabEndX - tabStartX) / 2)
            tabCenterY = rect.height() / 2
            tabCenter = QPoint(tabCenterX, tabCenterY)
            util.qtbot.mouseClick(self.qml, Qt.LeftButton, Qt.NoModifier, tabCenter)
            currentIndex = item.property('currentIndex')
            if not currentIndex == iTab:
                raise RuntimeError('Unable to click tab bar button index %i for TabBar %s. `currentIndex` is still %i' % (iTab, objectName, currentIndex))
    
        def itemProp(self, objectName, attr):
            item = self.findItem(objectName)
            return item.property(attr)
    

    You will also notice that I wrote in a dot-reference in findItem that walks down child items when a . is in the objectName, such as dateButtons.dateTextInput. Here is some pytest code to do some user input:

        if personName:
            ep.keyClicks('nameBox', personName)
            assert ep.itemProp('nameBox', 'currentText') == personName
        ep.keyClicks('dateButtons.dateTextInput', util.dateString(props['date']))
        # assertFalse(ui.dateEdit.lineEdit().hasFocus())                                                                                                                                                             
        if props['unsure'] != ep.itemProp('dateButtons', 'unsure'):
    	ep.keyClick('dateButtons.unsureBox', Qt.Key_Space)
        ep.keyClicks('descriptionEdit', props['description'])
        ep.keyClicks('locationEdit', props['location'])
        if props['nodal'] != ep.itemProp('nodalBox', 'checked'):
    	ep.keyClick('nodalBox', Qt.Key_Space)
        ep.clickTabWidgetPage('tabBar', 1)
        ep.findItem('notesEdit').selectAll()
        ep.keyClicks('notesEdit', props['notes'])
        ep.mouseClick('doneButton', Qt.LeftButton)
    

    And here is the respective pytest code to verify that the corresponding Qml Items' properties reflect the change:

        assert event.description() == props['description']
        assert event.date() == props['date']
        assert event.unsure() == (props['unsure'] == Qt.Checked)
        assert event.location() == props['location']
        assert event.nodal() == (props['nodal'] == Qt.Checked)
        assert event.notes() == props['notes']
    

    That should allow you to write C++ unit/integration tests that allow for covering qml components contained with the QtWidgets app. Sweet!


  • Lifetime Qt Champion

    Thanks for the detailed feedback !



  • @SGaist Yeah, this seems like an important cookbook recipe in the making. I wish I had the time to tidy it up for something like that.


Log in to reply