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

如何测试让函数按固定周期无限执行的Python装饰器?

测试周期性无限执行工作流的稳定方案

背景

我开发了一个run_periodically装饰器,可让被装饰的函数每隔指定秒数无限执行。

装饰器示例代码

@run_periodically(cycle_time=1) # 单位:秒
def action(exec_time):
    print(f"exec_time: {exec_time}")

执行输出效果

调用action后会持续输出执行时间,格式如下:

exec_time: 2023-02-28 12:01:09.075368
exec_time: 2023-02-28 12:01:10.075368
exec_time: 2023-02-28 12:01:11.075368
exec_time: 2023-02-28 12:01:12.075368
...

当前测试用例

我编写了如下测试用例,虽然能运行,但担心受系统时钟波动、负载影响导致不稳定:

from datetime import datetime, timedelta
import pytest

def prepare_action(stop_time: datetime): 
    counter = 0

    @run_periodically(cycle_time=0.001)
    def action(exec_time: datetime):
        nonlocal counter
        counter += 1
        if exec_time > stop_time:
            assert counter == 3
            exit(42)

    return action

def test_run_periodically():
    stop_time = datetime.utcnow() + timedelta(milliseconds=3)
    action = prepare_action(stop_time=stop_time)
    with pytest.raises(SystemExit) as pytest_wrapped_e:
        action(exec_time=datetime.utcnow() + timedelta(milliseconds=1))
    assert pytest_wrapped_e.type == SystemExit
    assert pytest_wrapped_e.value.code == 42

稳定测试这类无限工作流的方法

1. 依赖注入可控的时间源

直接依赖系统时钟是不稳定的核心原因,给装饰器添加时间提供者的注入点,测试时用模拟时间完全控制时间流逝:

改造装饰器

from datetime import datetime
import time

def run_periodically(cycle_time, time_provider=datetime.utcnow):
    def decorator(func):
        def wrapper(*args, **kwargs):
            while True:
                current_time = time_provider()
                func(current_time, *args, **kwargs)
                time.sleep(cycle_time)
        return wrapper
    return decorator

模拟时间的测试用例

class MockTimeProvider:
    def __init__(self, start_time):
        self.current_time = start_time
    
    def advance(self, delta):
        self.current_time += delta
    
    def get(self):
        return self.current_time

def test_run_periodically_with_mock_time():
    start_time = datetime.utcnow()
    mock_time = MockTimeProvider(start_time)
    stop_time = start_time + timedelta(milliseconds=3)
    counter = 0

    @run_periodically(cycle_time=0.001, time_provider=mock_time.get)
    def action(exec_time):
        nonlocal counter
        counter += 1
        if exec_time > stop_time:
            assert counter == 3
            exit(42)
    
    # 手动推进时间触发执行
    mock_time.advance(timedelta(milliseconds=1))
    action()
    mock_time.advance(timedelta(milliseconds=1))
    mock_time.advance(timedelta(milliseconds=1))
    
    with pytest.raises(SystemExit) as excinfo:
        mock_time.advance(timedelta(milliseconds=1))
    
    assert excinfo.value.code == 42
    assert counter == 3

2. 给测试添加超时兜底

即使保留真实时间逻辑,也要给测试设置超时,避免因系统负载过高导致测试无限挂起。用pytest的timeout标记即可:

@pytest.mark.timeout(5) # 超时时间设为5秒,覆盖预期执行时间
def test_run_periodically_with_timeout():
    stop_time = datetime.utcnow() + timedelta(milliseconds=3)
    action = prepare_action(stop_time=stop_time)
    with pytest.raises(SystemExit) as pytest_wrapped_e:
        action(exec_time=datetime.utcnow() + timedelta(milliseconds=1))
    assert pytest_wrapped_e.type == SystemExit
    assert pytest_wrapped_e.value.code == 42

3. 用线程隔离执行,主动终止工作流

把周期性执行逻辑放到后台线程,测试时主动发送终止信号,避免用exit()影响测试进程:

改造装饰器支持终止

import threading
from datetime import datetime

def run_periodically(cycle_time):
    def decorator(func):
        stop_event = threading.Event()
        
        def periodic_task():
            while not stop_event.is_set():
                current_time = datetime.utcnow()
                func(current_time)
                stop_event.wait(cycle_time)
        
        def wrapper():
            thread = threading.Thread(target=periodic_task, daemon=True)
            thread.start()
            return stop_event
        
        return wrapper
    return decorator

线程版测试用例

def test_run_periodically_with_thread():
    stop_after = timedelta(milliseconds=4)
    counter = 0

    @run_periodically(cycle_time=0.001)
    def action(exec_time):
        nonlocal counter
        counter += 1
    
    # 启动任务并获取终止信号
    stop_event = action()
    # 等待足够时间让任务执行
    threading.Event().wait(stop_after.total_seconds())
    # 主动终止任务
    stop_event.set()
    
    # 允许±1的误差,兼容系统调度延迟
    assert 2 <= counter <= 4

4. 解耦逻辑,拆分测试

把业务逻辑和周期调度逻辑分离,分别测试:

  • 单独测试业务函数的正确性,验证其输入输出是否符合预期;
  • 单独测试装饰器的调度逻辑,用模拟时间验证是否按指定间隔调用函数。

内容的提问来源于stack exchange,提问作者Nikodem Bienia

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 13:03:31