有什么方法可以将模拟对象伪装成Qt方法参数中可接受的PyQt对象吗?

问题描述 投票:0回答:1

这是 MRE。你需要安装

pytest
...事实上 pytest-qt 不会造成任何伤害。

import sys, pytest
from PyQt5 import QtWidgets
from unittest import mock 

class MyLineEdit(QtWidgets.QLineEdit):
    def __init__(self, parent):
        super().__init__(parent)
        self.setPlaceholderText('bubbles')

class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Pytest MRE")
        self.setMinimumSize(400, 200)
        layout = QtWidgets.QVBoxLayout()
        qle = MyLineEdit(self)
        layout.addWidget(qle)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

def test_MyLineEdit_sets_a_title():
    mock_window = mock.Mock(spec=QtWidgets.QWidget)

    print(f'mock_window.__class__ {mock_window.__class__}') # says "QWidget"
    print(f'type(mock_window) {type(mock_window)}') # says "mock.Mock"

    with mock.patch('PyQt5.QtWidgets.QLineEdit.setPlaceholderText') as mock_set_placeholder:
        # with mock.patch('PyQt5.QtWidgets.QLineEdit.__init__') as mock_super_init:
            my_line_edit = MyLineEdit(mock_window)

    mock_set_placeholder.assert_called_once()
    assert False

这说明了一个经常出现的问题,我不确定是否有解决方法:我想传递一个模拟作为 PyQt 类的方法的参数(在本例中是一个构造函数:它是传递模拟的行为到导致问题的超类

__init__
)。

通常,当我尝试运行此测试时,我得到:

E       TypeError: arguments did not match any overloaded call:
E         QLineEdit(parent: Optional[QWidget] = None): argument 1 has unexpected type 'Mock'
E         QLineEdit(contents: Optional[str], parent: Optional[QWidget] = None): argument 1 has unexpected type 'Mock'
test_grey_out_qle_exp.py:7: TypeError

我在很多情况下都遇到了这个问题:“有意外的类型‘Mock’”...

一次又一次,在测试 PyQt 应用程序期间,我发现我必须传递一个 real

QtWidget
(或其他真正的 PyQt 类对象)。这反过来可能会导致各种问题,最坏的情况是令人讨厌的间歇性“Windows 访问冲突”错误......取决于我试图从中获取结果的真实 PyQt 对象和模拟的组合类型。

我想以某种方式通过伪装将模拟传递到 PyQt 代码中。我尝试过

MagicMock
spec
autospec
等的各种组合。但是PyQt代码永远不会被愚弄。它似乎正在使用内置函数
type(x)
进行检查。但是,我不确定这一点,因为我无法检查任何源代码,因为 PyQt 模块似乎是从 C++ 代码自动生成的。

我想到的一件事:是否可以修补内置功能

type()
...?可能是一个不太可能的解决方案,尤其是因为我怀疑 PyQt 模块在检查类型时没有使用 Python 代码。不过我确实搜索过这个,但无济于事。

PS,如注释掉的

mock.patch...
行所示,一种可能的解决方法是开始修补任何导致问题的 PyQt 超类方法...

后来:musicamante的建议

好主意。我试过这个:

class HybridMock(QtWidgets.QWidget, mock.Mock):
     def __init__(self, *args, **kwargs):
        # super().__init__(*args, **kwargs)
        QtWidgets.QWidget.__init__(self, *args, **kwargs)

def test_MyLineEdit_sets_a_title():
    # mock_window = mock.Mock(spec=QtWidgets.QWidget)
    mock_window = HybridMock()

运行时

pytest
,这只是崩溃并显示“Python已停止工作......”(没有任何解释)。 NB平台是W10。我并不感到惊讶。

python pyqt mocking pytest pytest-qt
1个回答
0
投票

问:有什么方法可以将模拟对象伪装成 Qt 方法参数中可接受的 PyQt 对象吗?

答:没有。

这不仅仅是让“类型”将模拟对象报告为 Qtobject 的问题:QT 库实际上需要将对象的二进制内容传递为实际的 QTObject,其数据结构与 Qt 使用的一样。尝试用模拟对象替换 Qt 库的内部方法也是如此,比如

   with mock.patch('PyQt5.QtWidgets.QLineEdit.setPlaceholderText') as mock_set_placeholder:
行 - 你的里程可能会有所不同,有时你可能会很幸运。

因此,您所处的场景是 Python“MagicMock”对象(无论是本机对象还是 pytest 扩展中实现的对象)没有多大帮助。要做的事情是构建您自己的“模拟”对象,然后相应地附加到您的代码 - 然后您可以创建类,这些类本身就是 QTobject 的子类,但具有记录任何调用的中间方法(然后转发调用 Qt 库)。

在此示例中,如果您想断言正在调用某个方法的超类版本,则除了在测试时用模拟类替换该超类之外,没有其他方法。 (是的,在这种情况下,魔术模型可能会起作用)。

因此,您可以在运行时将

QtWidgets.QLineEdit
的超类替换为一个类,尽管该类也继承自
QLineEdit
,但替换其
__new__
方法以返回模拟对象。

在您的文本文件中,为

MyLineEdit
创建一个基类,如下所示:

from contextlib import contextmanager
...

QtWidgets.QLineEdit
class MockLineEdit(QtWidgets.QLineEdit):
    def __new__(cls, *args):
        global linemock
        linemock =  mock.Mock()
        return linemock
    def __init__(self, *args):
        # code for logging any needed calls here.
        #example:
        linemock.__init__(*args)
@contextmanager 
def patchclass(cls, replacement):
    try:
        original_bases = cls.__bases__
        cls.__bases__ = replacement
        yield cls
    finally:
        cls.__bases__ = original_bases


...
def test_MyLineEdit_sets_a_title():
    mockwindow = ...

    with mock.patch('PyQt5.QtWidgets.QLineEdit.setPlaceholderText') as mock_set_placeholder:
        # the mocked class cannot, at any point, touch  any "real" Qt functions  - it can't be passed to the "layout" object: we have to fake it.
        with mock.patch('QtWidgets.QVBoxLayout'):
            with patchclass(MyLineEdit, (MockLineEdit,)):
            
                 my_line_edit = MyLineEdit(mock_window)
                 # make any assetions on MyLineEdit superclass calls
                 # using the global `linemock` variable

MyLIneEdit
现在将成为模拟类 - 类中使用
super().
调用的任何方法都必须在模拟父类中手动编码。 不得使用此测试实例中的“self”进行“真正的”Qt 调用 - 必须对模拟对象进行任何此类调用(如
layout.addWidget(qle)
行)。

此外,这是一个简单的示例,使用全局变量来设置要在测试时进行内省的模拟对象 - 如果您并行运行测试,那将不起作用 - 使用

threading.local
contextvars.ContextVar()
容器,以便如果需要,您的测试代码可以访问在模拟超类
__new__
代码中创建的模拟实例。

© www.soinside.com 2019 - 2024. All rights reserved.