如何让pytest捕获Qt事件循环中抛出的ValueError异常?
问题
编写将数据保存为JSON到磁盘的集成测试时,预期行为是:当JSON文件中已存在目标记录时,应用需在覆盖前抛出ValueError异常。由于该异常属于预期行为,集成测试需捕获此异常以通过测试,但该异常在Qt事件循环中抛出时,pytest无法捕获,仅能在stderr中看到Qt事件循环捕获到的异常信息。
错误信息
pytest未捕获异常的报错
records = json.loads(database.read_text())["records"].keys() if record_name in records: > with pytest.raises(ValueError) as execinfo: E Failed: DID NOT RAISE <class 'ValueError'>
stderr中Qt事件循环捕获的异常
---------------------------- Captured stderr call ----------------------------- Exceptions caught in Qt event loop: ________________________________________________________________________________ Traceback (most recent call last): File "<string>", line 3, in save File "C:\Users\me\AppData\Local\Programs\Python\Python312\Lib\unittest\mock.py", line 1137, in __call__ return self._mock_call(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\me\AppData\Local\Programs\Python\Python312\Lib\unittest\mock.py", line 1141, in _mock_call return self._execute_mock_call(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\me\AppData\Local\Programs\Python\Python312\Lib\unittest\mock.py", line 1202, in _execute_mock_call result = effect(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\me\Desktop\ProjectDir\.venv\Lib\site-packages\pytest_mock\plugin.py", line 177, in wrapper r = method(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^ File "c:\Users\me\Desktop\ProjectDir\record\home.py", line 132, in save record.toJSON() File "c:\Users\me\Desktop\ProjectDir\record\record.py", line 53, in toJSON raise ValueError(f"record '{self.name}' already exists in database") ValueError: record 'existing_record_name' already exists in database
代码示例
集成测试代码
## code within the test_save_new_record() integration test records = json.loads(database.read_text())["records"].keys() if record_name in records: with pytest.raises(ValueError) as execinfo: qtbot.mouseClick(home_widget.save_button, Qt.MouseButton.LeftButton) # << exception SHOULD be raised else: # rest of test...
信号连接代码
class HomeWidget(QWidget, Ui_widget): def __init__(self): self.save_button.clicked.connect(self.save) # <<<<
槽函数代码
def save(self): selected_record = self.record_list.currentItem() if selected_record: self.record_builder.name_edited = selected_record.text() record = self.record_builder.build() record.toJSON() # <<<< this raises the exception
异常抛出代码
def toJSON(self): # if the record already exists, raise ValueError exception db = json.loads(self.record_db.read_text(encoding="UTF-8")) if self.name in db["records"].keys(): raise ValueError(f"record '{self.name}' already exists in database") # <<<< else: # rest of method ...
解决方法
方法1:直接调用槽函数,绕过Qt事件循环
Qt事件循环会自动捕获槽函数中抛出的异常并输出到stderr,不会将异常传递到测试线程。因此可以跳过信号触发,直接在测试中调用槽函数save(),这样异常会在测试的执行流中抛出,pytest就能正常捕获:
## 修改后的集成测试代码 records = json.loads(database.read_text())["records"].keys() if record_name in records: with pytest.raises(ValueError) as execinfo: home_widget.save() # 直接调用槽函数,而非通过按钮点击触发 # 可选:断言异常信息是否符合预期 assert f"record '{record_name}' already exists in database" in str(execinfo.value) else: # rest of test...
方法2:自定义Qt异常钩子,收集事件循环中的异常
如果必须通过按钮点击触发(比如要测试按钮的信号连接逻辑),可以修改Qt的异常处理钩子,将事件循环中捕获的异常存储起来,之后在测试中进行断言:
import sys from PyQt6.QtCore import QCoreApplication, Qt # 全局存储捕获的异常 captured_exceptions = [] def pytest_configure(): # 保存原始的异常钩子 original_excepthook = sys.excepthook def custom_excepthook(type, value, traceback): captured_exceptions.append((type, value, traceback)) original_excepthook(type, value, traceback) sys.excepthook = custom_excepthook # 在测试中使用 def test_save_new_record(qtbot, home_widget, database): record_name = "existing_record_name" # 确保记录已存在... qtbot.mouseClick(home_widget.save_button, Qt.MouseButton.LeftButton) qtbot.wait(100) # 等待事件循环处理完成 # 检查捕获的异常 assert len(captured_exceptions) > 0 exc_type, exc_value, _ = captured_exceptions[-1] assert exc_type == ValueError assert f"record '{record_name}' already exists in database" in str(exc_value) # 清空存储,避免影响其他测试 captured_exceptions.clear()
方法3:在槽函数中通过信号传递异常
修改业务代码,将槽函数中的异常通过自定义信号抛出,测试中监听该信号并断言:
# 修改HomeWidget类,添加自定义信号 from PyQt6.QtCore import pyqtSignal class HomeWidget(QWidget, Ui_widget): save_error = pyqtSignal(Exception) # 定义异常信号 def __init__(self): self.save_button.clicked.connect(self.save) def save(self): try: selected_record = self.record_list.currentItem() if selected_record: self.record_builder.name_edited = selected_record.text() record = self.record_builder.build() record.toJSON() except Exception as e: self.save_error.emit(e) # 抛出异常信号 # 测试代码 def test_save_new_record(qtbot, home_widget, database): record_name = "existing_record_name" records = json.loads(database.read_text())["records"].keys() if record_name in records: # 监听异常信号 with qtbot.waitSignal(home_widget.save_error, timeout=1000) as blocker: qtbot.mouseClick(home_widget.save_button, Qt.MouseButton.LeftButton) # 断言异常类型和信息 assert isinstance(blocker.args[0], ValueError) assert f"record '{record_name}' already exists in database" in str(blocker.args[0]) else: # rest of test...
内容的提问来源于stack exchange,提问作者David
相关产品推荐
相关产品推荐

