这是 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。我并不感到惊讶。
你几乎找到了解决办法!将模拟与
QtWidgets.QWidget
混合的问题在于,它会触发真实的
QWidget
初始化,这在单元测试环境中不希望发生。
与其混合
mock.Mock
,不如使用
QtBot
(
pytest-qt
包含的)来创建和测试你的 PyQt 应用程序:
import pytest
from PyQt5 import QtWidgets
from PyQt5.QtTest import QTest
from PyQt5.QtCore import Qt
@pytest.fixture
def window(qtbot):
window = Window()
qtbot.addWidget(window) # 确保窗口被清理干净
return window
def test_MyLineEdit_sets_a_title(window, qtbot):
# 使用 qtbot 找到 MyLineEdit
line_edit = window.findChild(MyLineEdit)
assert line_edit is not None
# 断言占位符文本
assert line_edit.placeholderText() == "bubbles"
在这个例子中:
-
qtbot
fixture :pytest-qt
提供的qtbot
fixture 让你可以与你的 PyQt 应用程序进行交互。 -
window
fixture :这个 fixture 创建了一个Window
实例,并使用qtbot.addWidget
将其添加到 Qt 的事件循环中。这确保了在测试结束后窗口被正确清理。 -
查找
MyLineEdit
:在你的测试中,你使用window.findChild(MyLineEdit)
来找到MyLineEdit
实例。 -
断言 :现在你可以直接对
MyLineEdit
对象进行断言,例如检查它的占位符文本。
使用这种方法,你不需要模拟
QWidget
,从而避免了混合模拟和真实 PyQt 对象的复杂性。
qtbot
负责处理事件循环和窗口清理等任务,让你专注于测试应用程序的逻辑。