在 python 中使用 pytest 测试基于 QML 的应用程序
Testing QML based app with pytest in python
我想用 Pytest 测试我的 QML 前端代码和我的 Python 后端代码(使用 PySide2),并且能够发送 keyClicks
、MouseClicks
和 signals
就像 pytest-qt plugin does. I have already checked out pytest-qml,但测试代码是通过 QML 编写的,然后只能通过 pytest 运行,但我想从 python 本身发送事件等,不是 QML
基本上,python 代码是这样的:
"""
Slots, Signals, context class etc etc...
"""
app = QGuiApplication([])
engine = QQmlApplicationEngine()
engine.load(QUrl.fromLocalFile("main.qml"))
app.exec_()
和一个简单的 main.qml
文件,
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
import QtQuick.Controls 2.15
ApplicationWindow {
id: mywin
width: Screen.desktopAvailableWidth
height: Screen.desktopAvailableHeight
visible: true
FileDialog {
id: openDialog
title: "mydialog"
onAccepted: {
}
}
Button {
objectName: "mybtn"
width: 200
height: 200
id: btn
text: "hello"
onClicked: {
openDialog.open()
}
}
}
我想做(伪代码)类似
的事情
def test_file_open():
#Grab QQuickItem(btn)
#Send mouse event to click btn
#Send string to file dialog
# assert string sent == string selected
pytest-qt
插件可以工作,但函数采用 QWidget
并且 QML 处理 QQuickItems
,据我所知它不处理 QWidgets。
是否有可能,或者我测试我的应用程序插槽等的唯一选择是通过 pytest-qml
?也许这是最简单的方法,但也许还有其他选择:)
编辑:
如果您使用 import Qt.labs.platform 1.1
而不是 import QtQuick.Dialogs 1.3
,并强制 QML 不 使用本机对话框,则只需更改
# assert myfiledialog.property("fileUrl").toLocalFile() == filename # uses QDialog
assert myfiledialog.property("currentFile").toLocalFile() == filename # using QLabs Dialog
然后使用接受的答案中的其余代码它会起作用,所以显然它不使用本机对话框非常重要。
如果以后有人知道如何使用本机对话并使用 QtQuick.Dialogs 1.3
作为最初提出的问题,那就太好了 :)。但是整体测试还是不错的!
您可以使用相同的 API,因为 pytest-qt 基于 QtTest。显然你必须了解应用程序的结构,例如 FileDialog 只是一个 QObject,它只管理一个具有对话框的 QWindow,此外还管理项目相对于 windows.[= 的位置。 13=]
import os
from pathlib import Path
from PySide2.QtCore import QUrl
from PySide2.QtQml import QQmlApplicationEngine
CURRENT_DIR = Path(__file__).resolve().parent
def build_engine():
engine = QQmlApplicationEngine()
filename = os.fspath(CURRENT_DIR / "main.qml")
url = QUrl.fromLocalFile(filename)
engine.load(url)
return engine
def main():
app = QGuiApplication([])
engine = build_engine()
app.exec_()
if __name__ == "__main__":
main()
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Dialogs 1.3
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
ApplicationWindow {
id: mywin
width: Screen.desktopAvailableWidth
height: Screen.desktopAvailableHeight
visible: true
FileDialog {
id: openDialog
objectName: "myfiledialog"
title: "mydialog"
onAccepted: {
}
}
Button {
id: btn
objectName: "mybtn"
width: 200
height: 200
text: "hello"
onClicked: {
openDialog.open();
}
}
}
import os
from PySide2.QtCore import QCoreApplication, QObject, Qt, QPointF
from PySide2.QtGui import QGuiApplication
from PySide2.QtQuick import QQuickItem
from PySide2.QtWidgets import QApplication
import pytest
from app import build_engine
@pytest.fixture(scope="session")
def qapp():
QCoreApplication.setOrganizationName("qapp")
QCoreApplication.setOrganizationDomain("qapp.com")
QCoreApplication.setAttribute(Qt.AA_DontUseNativeDialogs)
yield QApplication([])
def test_app(tmp_path, qtbot):
engine = build_engine()
assert QCoreApplication.testAttribute(Qt.AA_DontUseNativeDialogs)
with qtbot.wait_signal(engine.objectCreated, raising=False):
assert len(engine.rootObjects()) == 1
root_object = engine.rootObjects()[0]
root_item = root_object.contentItem()
mybtn = root_object.findChild(QQuickItem, "mybtn")
assert mybtn is not None
center = QPointF(mybtn.width(), mybtn.height()) / 2
qtbot.mouseClick(
mybtn.window(),
Qt.LeftButton,
pos=root_item.mapFromItem(mybtn, center).toPoint(),
)
qtbot.wait(1000)
qfiledialog = None
for window in QGuiApplication.topLevelWindows():
if window is not root_object:
qfiledialog = window
assert qfiledialog is not None, QGuiApplication.topLevelWindows()
file = tmp_path / "foo.txt"
file.touch()
filename = os.fspath(file)
for letter in filename:
qtbot.keyClick(qfiledialog, letter, delay=100)
qtbot.wait(1000)
qtbot.keyClick(qfiledialog, Qt.Key_Return)
qtbot.wait(1000)
myfiledialog = root_object.findChild(QObject, "myfiledialog")
assert myfiledialog is not None
assert myfiledialog.property("fileUrl").toLocalFile() == filename
注意:如果 filedialog 使用原生 window,测试可能会失败,您可以使用 pyinput 之类的工具,但更简单的选择是使用 virtualenv。
我想用 Pytest 测试我的 QML 前端代码和我的 Python 后端代码(使用 PySide2),并且能够发送 keyClicks
、MouseClicks
和 signals
就像 pytest-qt plugin does. I have already checked out pytest-qml,但测试代码是通过 QML 编写的,然后只能通过 pytest 运行,但我想从 python 本身发送事件等,不是 QML
基本上,python 代码是这样的:
"""
Slots, Signals, context class etc etc...
"""
app = QGuiApplication([])
engine = QQmlApplicationEngine()
engine.load(QUrl.fromLocalFile("main.qml"))
app.exec_()
和一个简单的 main.qml
文件,
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
import QtQuick.Controls 2.15
ApplicationWindow {
id: mywin
width: Screen.desktopAvailableWidth
height: Screen.desktopAvailableHeight
visible: true
FileDialog {
id: openDialog
title: "mydialog"
onAccepted: {
}
}
Button {
objectName: "mybtn"
width: 200
height: 200
id: btn
text: "hello"
onClicked: {
openDialog.open()
}
}
}
我想做(伪代码)类似
的事情def test_file_open():
#Grab QQuickItem(btn)
#Send mouse event to click btn
#Send string to file dialog
# assert string sent == string selected
pytest-qt
插件可以工作,但函数采用 QWidget
并且 QML 处理 QQuickItems
,据我所知它不处理 QWidgets。
是否有可能,或者我测试我的应用程序插槽等的唯一选择是通过 pytest-qml
?也许这是最简单的方法,但也许还有其他选择:)
编辑:
如果您使用 import Qt.labs.platform 1.1
而不是 import QtQuick.Dialogs 1.3
,并强制 QML 不 使用本机对话框,则只需更改
# assert myfiledialog.property("fileUrl").toLocalFile() == filename # uses QDialog
assert myfiledialog.property("currentFile").toLocalFile() == filename # using QLabs Dialog
然后使用接受的答案中的其余代码它会起作用,所以显然它不使用本机对话框非常重要。
如果以后有人知道如何使用本机对话并使用 QtQuick.Dialogs 1.3
作为最初提出的问题,那就太好了 :)。但是整体测试还是不错的!
您可以使用相同的 API,因为 pytest-qt 基于 QtTest。显然你必须了解应用程序的结构,例如 FileDialog 只是一个 QObject,它只管理一个具有对话框的 QWindow,此外还管理项目相对于 windows.[= 的位置。 13=]
import os
from pathlib import Path
from PySide2.QtCore import QUrl
from PySide2.QtQml import QQmlApplicationEngine
CURRENT_DIR = Path(__file__).resolve().parent
def build_engine():
engine = QQmlApplicationEngine()
filename = os.fspath(CURRENT_DIR / "main.qml")
url = QUrl.fromLocalFile(filename)
engine.load(url)
return engine
def main():
app = QGuiApplication([])
engine = build_engine()
app.exec_()
if __name__ == "__main__":
main()
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Dialogs 1.3
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
ApplicationWindow {
id: mywin
width: Screen.desktopAvailableWidth
height: Screen.desktopAvailableHeight
visible: true
FileDialog {
id: openDialog
objectName: "myfiledialog"
title: "mydialog"
onAccepted: {
}
}
Button {
id: btn
objectName: "mybtn"
width: 200
height: 200
text: "hello"
onClicked: {
openDialog.open();
}
}
}
import os
from PySide2.QtCore import QCoreApplication, QObject, Qt, QPointF
from PySide2.QtGui import QGuiApplication
from PySide2.QtQuick import QQuickItem
from PySide2.QtWidgets import QApplication
import pytest
from app import build_engine
@pytest.fixture(scope="session")
def qapp():
QCoreApplication.setOrganizationName("qapp")
QCoreApplication.setOrganizationDomain("qapp.com")
QCoreApplication.setAttribute(Qt.AA_DontUseNativeDialogs)
yield QApplication([])
def test_app(tmp_path, qtbot):
engine = build_engine()
assert QCoreApplication.testAttribute(Qt.AA_DontUseNativeDialogs)
with qtbot.wait_signal(engine.objectCreated, raising=False):
assert len(engine.rootObjects()) == 1
root_object = engine.rootObjects()[0]
root_item = root_object.contentItem()
mybtn = root_object.findChild(QQuickItem, "mybtn")
assert mybtn is not None
center = QPointF(mybtn.width(), mybtn.height()) / 2
qtbot.mouseClick(
mybtn.window(),
Qt.LeftButton,
pos=root_item.mapFromItem(mybtn, center).toPoint(),
)
qtbot.wait(1000)
qfiledialog = None
for window in QGuiApplication.topLevelWindows():
if window is not root_object:
qfiledialog = window
assert qfiledialog is not None, QGuiApplication.topLevelWindows()
file = tmp_path / "foo.txt"
file.touch()
filename = os.fspath(file)
for letter in filename:
qtbot.keyClick(qfiledialog, letter, delay=100)
qtbot.wait(1000)
qtbot.keyClick(qfiledialog, Qt.Key_Return)
qtbot.wait(1000)
myfiledialog = root_object.findChild(QObject, "myfiledialog")
assert myfiledialog is not None
assert myfiledialog.property("fileUrl").toLocalFile() == filename
注意:如果 filedialog 使用原生 window,测试可能会失败,您可以使用 pyinput 之类的工具,但更简单的选择是使用 virtualenv。