You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 09:57:32