Pytest如何对Mock对象内部不可访问的本地实例做断言验证
问题描述
在Pytest单元测试中,需要对无法从外层对象直接访问的本地实例对象进行Mock并执行断言校验。
最小复现示例
待Mock的类为Executor:
class Executor: def execute(self, func, *args): func(*args)
Mock Executor的fixture实现:
import pytest @pytest.fixture def executor(mocker): mocker.patch('executor.Executor.execute') return Executor()
基础场景下可验证Executor.execute被正确调用:
def test_execute(executor): func = lambda x: x**2 executor.execute(func, 7) Executor.execute.assert_called_once_with(func, 7) # 以下场景会失败:当无法获取传入的lambda实例时如何完成测试 # func2 = lambda y:y**2 # Executor.execute.assert_called_once_with(func2, 7)
更通用的复杂参数场景:
def test_execute_with_complex_args(executor): def somefunc(x, y): x.increment(y) class SomeObj: x=30 def increment(self, y): self.x += y obj = SomeObj() executor.execute(somefunc, obj, 7) Executor.execute.assert_called_once() # 以下断言仅在可获取somefunc、obj实例时生效 Executor.execute.assert_called_once_with(somefunc, obj, 7) # 当无法访问somefunc、obj实例时,如何验证increment被传入参数7调用?
实际业务场景
测试基于SqlAlchemy实现的数据库客户端时,需要验证查询语句携带正确参数执行,客户端典型方法如下:
def get_job(self, job_id: UUID) -> Job: return run_transaction(sessionmaker(bind=self.engine), lambda s: copy(s.query(Job).filter(Job.id == job_id).one()))
其中sessionmaker和run_transaction可直接Mock测试,但需要验证事务内部Session.query方法是否被正确调用,现有Mock方式无法覆盖这类内部调用的验证需求。
解决方案
核心思路:不要直接把外层拦截方法Mock成无逻辑的哑对象,通过Mock的side_effect配置自定义拦截逻辑,在捕获调用记录的同时拿到内部传参、执行内部逻辑,即可完成无实例引用场景下的断言。
通用处理方式
- 如果不需要执行内部真实逻辑:调用完成后直接访问Mock对象的
call_args属性,就能拿到所有历史调用的位置参数、关键字参数,不需要提前在测试用例中持有参数实例引用。def test_no_ref(executor): # 执行业务调用,内部传入的func、obj对测试用例不可见 call_business_method() # 直接从mock对象取入参 args, kwargs = Executor.execute.call_args called_func, called_first_arg = args[0], args[1] # 后续可以对拿到的参数做任意校验,比如检查函数源码、手动传参验证行为 - 如果需要校验内部调用链行为:给外层Mock配置
side_effect,拦截到调用后手动执行传入的函数/逻辑,同时提前Mock函数内部依赖的对象,即可捕获完整调用链的执行记录。
示例1:本地函数内部调用校验
def test_execute_inner_call(mocker): # 初始化Mock,不要直接截断execute逻辑 mock_execute = mocker.patch('executor.Executor.execute') executor = Executor() # 提前Mock内部会调用的目标方法 mock_increment = mocker.patch.object(SomeObj, 'increment') # 配置side_effect,拿到入参后手动执行传入的函数 def execute_side_effect(func, *args): func(*args) mock_execute.side_effect = execute_side_effect # 执行业务代码(模拟内部生成somefunc、obj的不可见场景) call_business_execute() # 直接断言内部方法的调用情况,不需要持有somefunc、obj实例 mock_increment.assert_called_once_with(7)
示例2:SqlAlchemy事务内部逻辑校验
针对数据库客户端场景,不需要让Mock的run_transaction直接返回固定值,配置side_effect主动执行传入的事务lambda,同时传入提前构造好的Mock Session对象,即可捕获Session上的所有调用:
def test_get_job(mocker): test_job_id = UUID("替换为测试用ID") # 提前构造Mock Session mock_session = mocker.MagicMock() # Mock session工厂 mock_sessionmaker = mocker.patch('your_client_module.sessionmaker') mock_sessionmaker.return_value = lambda: mock_session # Mock事务方法 mock_run_tx = mocker.patch('your_client_module.run_transaction') # 配置side_effect,执行事务内部的lambda def run_tx_side_effect(session_factory, tx_func): return tx_func(mock_session) mock_run_tx.side_effect = run_tx_side_effect # 执行被测方法 db_client.get_job(test_job_id) # 直接断言Session查询逻辑是否符合预期 mock_session.query.assert_called_once_with(Job) mock_session.query.return_value.filter.assert_called_once_with(Job.id == test_job_id)
内容的提问来源于stack exchange,提问作者Najeeb
相关产品推荐
相关产品推荐

