这是 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。我并不感到惊讶。
问:有什么方法可以将模拟对象伪装成 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__
代码中创建的模拟实例。