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

如何让Pytest测试在调用未await的async函数时自动失败?

如何检测Pytest异步测试中未被await的async函数调用

原方案的问题

你提供的ChatGPT代码无法生效,因为它仅检查了测试函数自身返回的协程,完全没有处理测试函数内部调用async函数生成的协程对象。对于test_await内部的func()调用,该装饰器根本无法感知到,自然不会触发失败。

可行方案

下面提供两种实现思路,分别基于静态代码分析和运行时跟踪,都能满足你的需求。

方案一:静态代码分析(AST)

通过解析测试函数的抽象语法树(AST),提前检测所有async函数调用是否被await包裹。这种方式在测试执行前就能发现问题,效率较高。

import ast
import inspect
import functools

def checkawait(test_func):
    @functools.wraps(test_func)
    async def wrapper(*args, **kwargs):
        # 获取测试函数的源代码
        try:
            source = inspect.getsource(test_func)
        except OSError:
            # 处理无法获取源代码的情况(如动态生成的函数)
            return await test_func(*args, **kwargs)
        
        # 解析AST
        tree = ast.parse(source)
        
        # 遍历AST节点,检查未被await的async函数调用
        class CoroutineCallChecker(ast.NodeVisitor):
            def visit_Call(self, node):
                # 判断当前调用是否被await包裹
                is_awaited = isinstance(getattr(node, 'parent', None), ast.Await)
                
                # 尝试获取被调用的函数对象
                func_obj = None
                try:
                    if isinstance(node.func, ast.Name):
                        # 处理直接调用全局/局部函数的情况(如func())
                        func_name = node.func.id
                        func_obj = test_func.__globals__.get(func_name)
                        if not func_obj:
                            func_obj = test_func.__closure__[0].cell_contents.get(func_name) if test_func.__closure__ else None
                    elif isinstance(node.func, ast.Attribute):
                        # 处理属性调用的情况(如obj.method())
                        if isinstance(node.func.value, ast.Name):
                            obj_name = node.func.value.id
                            obj = test_func.__globals__.get(obj_name)
                            if obj:
                                func_obj = getattr(obj, node.func.attr, None)
                except (AttributeError, IndexError, KeyError):
                    pass
                
                # 检测到未被await的async函数调用
                if func_obj and inspect.iscoroutinefunction(func_obj) and not is_awaited:
                    raise AssertionError(f"Async function '{func_obj.__name__}' was called without await")
                
                self.generic_visit(node)
        
        # 给所有AST节点添加parent属性,用于判断是否被await包裹
        for node in ast.walk(tree):
            for child in ast.iter_child_nodes(node):
                child.parent = node
        
        # 执行检查
        checker = CoroutineCallChecker()
        checker.visit(tree)
        
        # 执行原测试函数
        return await test_func(*args, **kwargs)
    
    return wrapper

优缺点:

  • 优点:无需运行测试逻辑即可提前发现问题,性能开销小;错误信息清晰,直接指出未被await的函数名。
  • 缺点:无法处理动态生成的函数、闭包内的函数或间接引用的函数(如通过变量调用async函数);对复杂语法场景的支持有限。

方案二:运行时跟踪(sys.settrace)

通过Python的sys.settrace钩子在测试执行期间跟踪所有函数调用,记录生成的协程对象,并检查这些协程是否被await处理。这种方式能覆盖更多动态场景。

import sys
import asyncio
import inspect
import functools

def checkawait(test_func):
    @functools.wraps(test_func)
    async def wrapper(*args, **kwargs):
        created_coroutines = set()
        awaited_coroutines = set()
        
        def trace_handler(frame, event, arg):
            if event == 'call':
                # 检查当前调用的是否是async函数
                func_name = frame.f_code.co_name
                func_obj = frame.f_globals.get(func_name)
                if func_obj and inspect.iscoroutinefunction(func_obj):
                    # async函数调用会返回协程对象,arg即为该对象
                    if inspect.iscoroutine(arg):
                        created_coroutines.add(arg)
            elif event == 'yield':
                # 当await协程时,arg通常是被await的协程对象
                if inspect.iscoroutine(arg):
                    awaited_coroutines.add(arg)
            return trace_handler
        
        # 设置跟踪钩子
        original_trace = sys.settrace(trace_handler)
        try:
            # 执行原测试函数
            result = await test_func(*args, **kwargs)
        finally:
            # 恢复原跟踪钩子
            sys.settrace(original_trace)
        
        # 检查未被await的协程
        unawaited = created_coroutines - awaited_coroutines
        if unawaited:
            raise AssertionError(f"Found {len(unawaited)} unawaited coroutine(s)")
        
        return result
    
    return wrapper

优缺点:

  • 优点:能检测动态生成的函数、间接调用的async函数;覆盖更多运行时场景。
  • 缺点:运行时跟踪会带来一定性能开销;无法区分“故意不await(如创建后台任务)”和“忘记await”的情况,需要额外逻辑处理例外。

验证示例

使用任意一种装饰器后,你的测试代码会触发失败:

async def func():
    return 42

@checkawait
async def test_await():
    func()  # 未加await,测试会抛出AssertionError

执行该测试时,会直接报错并终止测试,符合你的需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 22:21:38