unittest.AsyncMock设置side_effect未抛异常却返回协程的问题咨询
问题背景与可复现代码
基于Python 3.11,仅依赖pytest的最小可复现示例:
# minum_reproducible_example.py from typing import Literal from unittest.mock import Mock, patch import pytest class CustomException(Exception): pass class Performance: status: Literal["ok", "failed"] score: int def __init__(self, status, score) -> None: self.status = status self.score = score def get_score(self): if self.status == "ok": return self.score else: raise Exception("Failed") async def get_performance(): return Performance(status="done", score=100) async def main(): performance = await get_performance() return performance.get_score() async def test_main(): with patch("minum_reproducible_example.get_performance") as mocked_get_performance: # 此写法失败 mocked_get_performance.return_value.get_score.side_effect = CustomException( "Mocked to always fail" ) with pytest.raises(CustomException): assert await main() == 100
遇到的问题
尝试patch异步函数get_performance,使其返回对象的get_score方法始终抛出异常,但实际未抛出异常,而是得到coroutine object AsyncMockMixin._execute_mock_call。根据官方文档,调用AsyncMock会返回异步函数,需await才会触发side_effect或return_value,且默认AsyncMock.return_value返回的是AsyncMock。
显式将get_score设置为Mock则可正常运行:
async def test_main(): with patch("minum_reproducible_example.get_performance") as mocked_get_performance: # 此写法有效 mocked_get_performance.return_value.get_score = Mock( side_effect=CustomException("Mocked to always fail") ) with pytest.raises(CustomException): assert await main() == 100
疑问
- 这种显式设置为Mock的方式是否可行?
- 是否有更优方案?
- 是否存在对AsyncMock的理解误区?
解答
1. 显式设置为Mock的方式完全可行
因为get_score是同步方法,用普通的Mock来模拟完全符合它的方法类型,不会有任何问题。这种写法逻辑清晰,直接匹配了原方法的同步特性,测试行为和预期一致,完全可以使用。
2. 更优方案:明确指定返回对象的类型(替代默认AsyncMock)
因为patch异步函数时,默认返回的return_value是AsyncMock,但我们需要的是一个带有同步get_score方法的普通对象,所以可以直接创建一个Mock实例作为get_performance的返回值,再给它的get_score属性设置side_effect,这样更符合语义:
async def test_main(): with patch("minum_reproducible_example.get_performance") as mocked_get_performance: # 创建普通Mock作为返回对象,而非默认的AsyncMock mock_performance = Mock() mock_performance.get_score.side_effect = CustomException("Mocked to always fail") mocked_get_performance.return_value = mock_performance with pytest.raises(CustomException): await main()
另外,也可以用MagicMock(Mock的子类,默认实现了更多魔法方法),效果和Mock一致,但在更复杂场景下更灵活。
3. 你对AsyncMock的理解误区
核心问题在于:AsyncMock是用来模拟异步函数/方法的,而你的get_score是同步方法。
当你patch异步函数get_performance时,mocked_get_performance本身是AsyncMock,它的return_value默认也是AsyncMock——这意味着mocked_get_performance.return_value.get_score默认是一个异步Mock方法,调用它会返回协程对象,而不是直接执行side_effect。但原代码中get_score是同步方法,直接调用不需要await,所以你试图给异步Mock设置side_effect后,调用它得到的是协程对象,自然不会抛出异常,反而会得到你看到的coroutine object。
总结:AsyncMock只应该用来模拟异步方法,同步方法用普通Mock/MagicMock即可,不要混用。
内容的提问来源于stack exchange,提问作者largehadroncollider

