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!
-
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.
-
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 theobjectName
property withqquickWidget->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.
- For keyboard events you have to manually focus the QQuickItem using either
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 theid
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 otherItem
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 asdateButtons.dateTextInput
. Here is somepytest
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!
- Obtain the
-
Thanks for the detailed feedback !