使用pytest-mock mock类方法时值解包报错的问题求解
问题根因
你遇到的解包错误本质是mock完全没生效,_get_update_sql走了原生逻辑没返回你预设的元组,叠加原代码本身存在两个低级bug:
- mock用法完全错误:
mocker.Mock()是用来创建普通mock对象的,根本没有patch方法,你写的mock_manager.patch(...)等于没执行,类的方法根本没被替换。patch是pytest-mock的mockerfixture直接提供的方法,不需要额外新建Mock实例调用。 - 原MyClass代码存在语法和属性拼写错误:
__init__方法定义漏了第一个参数self,正常实例化就会抛参数不匹配错误__init__里赋值的是self.update_type_id(无下划线前缀),但后续所有方法里调用的都是self._update_type_id(带前缀下划线),就算mock正常,这里也会抛属性不存在的错误
- 就算跳过上面的问题,patch路径错误也会导致mock不生效:patch必须指定方法在被测模块中的完整路径,直接写
MyClass._get_update_sql找不到目标,补丁打不上。
正确实现
首先先修复MyClass里的基础bug,不然测试永远跑不通:
import logging from typing import Tuple from psycopg2 import Error # 对应代码里捕获的数据库Error类型 logger = logging.getLogger(__name__) class MyClass: # 补全self参数 def __init__(self, update_type_id='1'): # 统一属性名,和后续调用的带下划线版本保持一致 self._update_type_id = update_type_id self._cursor = <database_connection>.cursor() # 替换为实际的数据库连接获取逻辑 def update_start_dt(self): self._update_job_ctrl_start_dt() def _update_job_ctrl_start_dt(self): update_sql, v_1 = self._get_update_sql(self._update_type_id) logger.debug(f'Update sql: {update_sql} and v_1: {v_1}') def _get_update_sql(self, update_type_id: int) -> Tuple: sql = f"SELECT start_sql, end_sql FROM <database.table> where update_type_key = {self._update_type_id}" self._run_sql(sql) record = self._cursor.fetchone() if record: return record else: logger.error(f'Record Not Found. Update type key ({update_type_id}) not found in the table in the database') raise Exception def _run_sql(self, sql_statement: str): try: self._cursor.execute(sql_statement) except (Exception, Error) as e: logger.error(f'Error {e} encountered when reading from table') raise e
测试代码提供两种常用方案,按需选择即可:
方案1:mock内部私有方法,隔离下层逻辑(适合快速验证方法调用链路)
这种方式不需要碰数据库相关逻辑,只验证update_start_dt的调用流是否正确:
# 替换为你自己的类导入路径 from my_module import MyClass class TestMyClass: # 测试方法第一个参数必须是self def test_update_start_dt(self, mocker, mock_get_connection, mock_run_sql): # 直接用mocker fixture打补丁,路径写类所在的完整模块路径 # autospec=True会自动匹配原方法签名,避免传参错误 mock_get_update_sql = mocker.patch( 'my_module.MyClass._get_update_sql', return_value=('123', '234') ) # mock日志避免无关输出,同时可以做断言 mock_log = mocker.patch('my_module.logger.debug') # patch完成后再实例化类,实例会继承被mock的方法 obj = MyClass(update_type_id='1') obj.update_start_dt() # 断言验证行为符合预期 mock_get_update_sql.assert_called_once_with('1') mock_log.assert_called_once_with('Update sql: 123 and v_1: 234')
方案2:仅mock外部数据库依赖,不mock类内部方法(适合更贴近真实逻辑的测试)
如果你想覆盖内部私有方法的逻辑,只隔离真实数据库连接,可以直接mock游标返回:
from my_module import MyClass class TestMyClass: def test_update_start_dt_with_real_internal_logic(self, mocker, mock_get_connection): # 构造mock游标,预设fetchone返回值 mock_cursor = mocker.MagicMock() mock_cursor.fetchone.return_value = ('123', '234') # 让数据库连接的cursor方法返回我们的mock游标 mock_get_connection.cursor.return_value = mock_cursor mock_log = mocker.patch('my_module.logger.debug') obj = MyClass(update_type_id='1') obj.update_start_dt() # 验证SQL正确执行 mock_cursor.execute.assert_called_once() executed_sql = mock_cursor.execute.call_args[0][0] assert 'SELECT start_sql, end_sql' in executed_sql assert 'update_type_key = 1' in executed_sql # 验证日志输出正确 mock_log.assert_called_once_with('Update sql: 123 and v_1: 234')
避坑提示
- patch路径必须写被测类所在模块的完整导入路径,比如你的类在
src/db/my_class.py里,导入路径是src.db.my_class.MyClass,patch路径就要写这个字符串,不能直接写类名。 - 如果不想记patch路径规则,可以实例化对象之后直接给实例绑定mock方法:
obj._get_update_sql = mocker.Mock(return_value=('123','234')),这种方式不会影响其他测试用例,更灵活。 - 写测试前先跑通被测代码的基础逻辑,先排除语法错误、属性名拼写错误这类和mock无关的问题,不然排查方向会走偏。
内容的提问来源于stack exchange,提问作者Adarsh
相关产品推荐
相关产品推荐

