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

测试Asyncio ensure_future异步回调方法遇断言错误,求指导

异步回调方法单元测试问题排查与解决

问题说明

我有一个发起回调请求的异步方法,编写单元测试时遇到以下错误:

FAILED tests/test_async_mapper_controller.py::test_make_callback - AssertionError: expected call not found.

原异步方法代码

async def make_callback(
    async_call_back_request: AsyncCallbackRequest, url=None, url_suffix=None
):
    if not (url or _config.default_callback_url):
        return
    elif not url:
        url = _config.default_callback_url

    asyncio.ensure_future(
        _callback(async_call_back_request, url=url, url_suffix=url_suffix)
    )

async def _callback(
    async_call_back_request: AsyncCallbackRequest, url, url_suffix=None
):
    try:
        res = httpx.post(
            f"{url.rstrip('/')}{url_suffix}",
            headers={"content-type": "application/json"},
            content=async_call_back_request.model_dump_json(),
            timeout=_config.default_callback_timeout,
        )

        res.raise_for_status()
    except Exception as e:
        _logger.error(f"Error during callback: {e}")

当前测试代码

@pytest.mark.asyncio
async def test_make_callback():
    async_call_back_request = AsyncCallbackRequest(
        header=AsyncCallbackRequestHeader(
            message_id="123",
            message_ts="2021-05-01T12:00:00Z",
            action="test_action",
            status=StatusEnum.succ,
        ),
        message={"key": "value"},
    )

    url = "http://test.com/callback"
    url_suffix = "/suffix"

    with patch(
        "openg2p_spar_mapper_api.controllers.async_mapper_controller.httpx.post"
    ) as mock_post:
        mock_response = MagicMock()
        mock_response.raise_for_status.return_value = None
        mock_post.return_value = mock_response

        # Execute the make_callback method
        task = asyncio.ensure_future(
            AsyncMapperController.make_callback(
                async_call_back_request, url, url_suffix
            )
        )

        # Wait for the make_callback task to complete
        await task

        # This line waits for all other spawned tasks to complete
        await asyncio.gather(
            *[t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
        )

        # Check if httpx.post was called as expected
        mock_post.assert_called_once_with(
            f"{url.rstrip('/')}{url_suffix}",
            headers={"content-type": "application/json"},
            content=async_call_back_request.model_dump_json(),
            timeout=30,
        )

问题根源与修复方案

1. 异步任务等待逻辑不可靠

make_callback仅通过asyncio.ensure_future启动_callback任务就直接返回,你用await asyncio.gather(...)收集所有任务的方式容易误包含测试框架的其他任务,导致_callback未执行完成就进入断言环节。

修复方式:直接捕获asyncio.ensure_future创建的_callback协程,手动执行确保完成:

@pytest.mark.asyncio
async def test_make_callback():
    async_call_back_request = AsyncCallbackRequest(
        header=AsyncCallbackRequestHeader(
            message_id="123",
            message_ts="2021-05-01T12:00:00Z",
            action="test_action",
            status=StatusEnum.succ,
        ),
        message={"key": "value"},
    )

    url = "http://test.com/callback"
    url_suffix = "/suffix"

    with patch("asyncio.ensure_future") as mock_ensure_future, \
         patch("openg2p_spar_mapper_api.controllers.async_mapper_controller.httpx.post") as mock_post:
        mock_response = MagicMock()
        mock_response.raise_for_status.return_value = None
        mock_post.return_value = mock_response

        # 执行make_callback
        await AsyncMapperController.make_callback(async_call_back_request, url, url_suffix)

        # 确认ensure_future被调用,并获取_callback协程执行
        assert mock_ensure_future.called
        callback_coroutine = mock_ensure_future.call_args.args[0]
        await callback_coroutine

        # 断言调用是否符合预期
        mock_post.assert_called_once_with(
            f"{url.rstrip('/')}{url_suffix}",
            headers={"content-type": "application/json"},
            content=async_call_back_request.model_dump_json(),
            timeout=_config.default_callback_timeout,
        )

2. Timeout参数硬编码不匹配

测试中断言用了timeout=30,但实际代码使用的是_config.default_callback_timeout,如果配置值不是30就会导致断言失败。

修复方式:二选一即可:

  • 补丁配置值:
    with patch("openg2p_spar_mapper_api.controllers.async_mapper_controller._config.default_callback_timeout", 30):
        # 测试代码...
        mock_post.assert_called_once_with(..., timeout=30)
    
  • 直接引用配置变量:
    mock_post.assert_called_once_with(..., timeout=_config.default_callback_timeout)
    

3. (可选)修复异步方法中的同步IO问题

当前_callback里用了同步的httpx.post,在异步函数中会阻塞事件循环,建议改用异步客户端:

async def _callback(
    async_call_back_request: AsyncCallbackRequest, url, url_suffix=None
):
    try:
        async with httpx.AsyncClient() as client:
            res = await client.post(
                f"{url.rstrip('/')}{url_suffix}",
                headers={"content-type": "application/json"},
                content=async_call_back_request.model_dump_json(),
                timeout=_config.default_callback_timeout,
            )
        res.raise_for_status()
    except Exception as e:
        _logger.error(f"Error during callback: {e}")

对应的测试需要调整补丁路径为openg2p_spar_mapper_api.controllers.async_mapper_controller.httpx.AsyncClient.post。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 22:50:10