在 python 中使用 pytest 测试基于 QML 的应用程序

Testing QML based app with pytest in python

我想用 Pytest 测试我的 QML 前端代码和我的 Python 后端代码(使用 PySide2),并且能够发送 keyClicksMouseClickssignals 就像 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。