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

如何在PyTest中测试KeyboardInterrupt异常处理逻辑

PyTest测试KeyboardInterrupt处理逻辑的实现方案

问题背景

假设主文件main.py中定义了如下函数:

# main.py
import time

def little_sleep(num):
   time.sleep(float(f'0.{i}'))  # 注意:此处原代码存在变量引用bug,i未在函数作用域定义

def wait():
    i = 0
    while True:
        try:
            little_sleep(i)
        except KeyboardInterrupt:
            print("Uh Oh!! You really got me there, I guess I'll just have to exit then...")
            break
    return

需求为使用PyTest对该模块所有函数编写单元测试,核心难点为:如何测试KeyboardInterrupt相关处理逻辑,确保既不会对其他测试用例造成影响,也不会干扰被测内部函数的正常运行。

初始编写的错误测试用例如下:

# test_main.py
import main
from multiprocessing import Process
import os, time, signal

def test_wait(capfd):
    process = Process(target= main.wait)
    process.start()
    time.sleep(1)
    
    # 尝试发送CTRL+C事件,偶发会直接终止little_sleep,导致异常无法被except捕获
    os.kill(process.pid, signal.CTRL_C_EVENT)

    # 直接kill进程会强制终止,无法走到打印逻辑
    process.kill()

    captured = capfd.readouterr()
    assert captured.out == "Uh Oh!! You really got me there, I guess I'll just have to exit then...\n"

运行测试后报错如下:

>> pytest test_main.py

collected 1 item
test_main.py F 

=============================== FAILURES ===================================
_______________________________ test_wait __________________________________

capfd = <_pytest.capture.CaptureFixture object at 0x000001F89F7BD400>

    def test_wait(capfd):
        ...
        captured = capfd.readouterr()
>       assert captured.out == "Uh Oh!! You really got me there, I guess I'll just have to exit then...\n"
E       AssertionError: assert '' == 'Uh Oh!! You ...xit then...\n'
E         - Uh Oh!! You really got me there, I guess I'll just have to exit then...

test_main.py:19: AssertionError

失败原因分析

初始用例失败有三个核心原因:

  • capfd只能捕获当前测试进程的标准输出,子进程运行产生的输出不会被自动捕获,因此读到的输出为空
  • signal.CTRL_C_EVENT仅在Windows控制台环境生效,跨平台兼容性差,信号发送时机不对时会直接终止底层C库的sleep调用,无法被Python层捕获为KeyboardInterrupt
  • process.kill()发送的是强制终止信号,进程会被操作系统直接回收,完全不会执行后续的异常处理、打印逻辑

可行解决方案

方案1:Mock模拟异常(单元测试优先推荐)

单元测试的核心是验证业务逻辑本身的正确性,不需要测试操作系统、Python解释层的信号传递逻辑,因此可以直接通过mock模拟little_sleep抛出KeyboardInterrupt,完全不需要多进程,执行速度快、无偶发失败问题。

首先修复原代码的变量引用bug:

# 修正后的little_sleep,使用传入的参数num而非未定义的i
def little_sleep(num):
   time.sleep(float(f'0.{num}'))

测试用例代码:

# test_main.py
import main
from unittest import mock

def test_wait_keyboard_interrupt(capfd):
    # 模拟little_sleep调用时抛出KeyboardInterrupt
    with mock.patch("main.little_sleep", side_effect=KeyboardInterrupt):
        main.wait()
    
    captured = capfd.readouterr()
    assert captured.out.strip() == "Uh Oh!! You really got me there, I guess I'll just have to exit then..."

方案2:真实信号触发(适合集成测试场景)

如果需要验证真实的键盘中断信号触发逻辑,可以修正多进程方案的问题:

  • 跨平台适配信号类型:Windows发送CTRL_C_EVENT,Linux/macOS发送SIGINT
  • 子进程内重定向标准输出到可跨进程读取的缓冲区,不依赖父进程的capfd
  • 发送信号后等待进程正常退出,超时后再强制清理,避免测试挂死

测试用例代码:

# test_main.py
import main
import sys
import time
import signal
import os
from multiprocessing import Process
from io import StringIO

def _run_wait_target(output_buf):
    # 子进程内重定向标准输出到传入的缓冲区
    sys.stdout = output_buf
    main.wait()

def test_wait_real_interrupt():
    output_buf = StringIO()
    proc = Process(target=_run_wait_target, args=(output_buf,))
    proc.start()
    # 等待进程进入wait循环
    time.sleep(0.5)

    # 按平台发送对应中断信号
    if sys.platform == "win32":
        os.kill(proc.pid, signal.CTRL_C_EVENT)
    else:
        os.kill(proc.pid, signal.SIGINT)
    
    # 等待进程正常退出,最多等待3秒
    proc.join(timeout=3)
    # 超时后强制清理,避免测试挂死
    if proc.is_alive():
        proc.kill()
        proc.join()
    
    output = output_buf.getvalue()
    assert output.strip() == "Uh Oh!! You really got me there, I guess I'll just have to exit then..."

注意事项

  • 单元测试场景优先选择mock方案,避免多进程带来的平台兼容、偶发失败、执行速度慢的问题
  • 真实信号测试用例建议归类到集成测试套件,不要混入核心单元测试流程
  • 测试前必须修复原代码中little_sleep函数的变量引用错误,否则代码运行会直接抛出NameError

内容的提问来源于stack exchange,提问作者Aman Ahmed Siddiqui

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 08:48:18