Python单元测试单独运行正常、批量运行后续测试失败问题求助
所有单元测试单独运行时均可正常通过,但批量运行时,第一个测试总能通过,第二个测试必然失败——哪怕调换测试代码顺序也无法解决。第二个测试中被mock的函数返回的始终是MagicMock对象,这是导致测试失败的核心原因。尝试使用mock.mock_reset()方法重置mock,但未能解决问题。
附带测试代码:
import copy import pytest from unittest.mock import patch, ANY import boto3 import os import test_constants as constants from datetime import datetime, timedelta from constants import CW_QUERY @pytest.fixture(autouse=True) def aws_credentials(): """Mocked AWS Credentials, to ensure we're not touching AWS directly""" os.environ['AWS_ACCESS_KEY_ID'] = 'testing' os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing' os.environ['AWS_SECURITY_TOKEN'] = 'testing' os.environ['AWS_SESSION_TOKEN'] = 'testing' os.environ['AWS_DEFAULT_REGION'] = 'testing' @patch("boto3.client") def test_get_sftp_user_details(mock_boto_client): from transfer_cli import get_sftp_user_details mock_boto_client = mock_boto_client.return_value mock_boto_client.describe_user.return_value = constants.T_RESPONSE_DESCRIBE_USER result = get_sftp_user_details(constants.SFTP_SERVER_ID, constants.USER_NAME) assert result == constants.T_RESPONSE_DESCRIBE_USER @patch("boto3.client") def test_get_transfer_users(mock_boto_client): from transfer_cli import get_transfer_users mock_boto_client = mock_boto_client.return_value mock_boto_client.list_users.return_value = constants.T_RESPONSE_LIST_USERS result = get_transfer_users(constants.SFTP_SERVER_ID) assert result == constants.T_RESPONSE_LIST_USERS["Users"]
核心问题出在Python的模块导入缓存机制:
你的测试虽然在每个函数内部写了from transfer_cli import xxx,但transfer_cli.py本身大概率是在模块顶部直接导入了boto3.client(比如import boto3后使用boto3.client)。当第一个测试运行时,transfer_cli模块被首次导入,此时@patch("boto3.client")生效,模块内的boto3.client引用被替换为mock对象。而第二个测试运行时,Python会复用已经缓存的transfer_cli模块,模块内的boto3.client还是第一个测试的mock实例——第二次的patch只修改了全局的boto3.client,但模块内已经缓存了旧的引用,新的patch无法生效,导致测试中配置的mock返回值不生效,最终拿到的是原始的MagicMock对象。
mock.mock_reset()无效的原因是它仅重置当前mock的状态,无法更新模块内已缓存的旧引用。
方案1:正确patch目标模块内的引用(推荐)
不要patch全局的boto3.client,而是patchtransfer_cli模块里实际使用的boto3.client引用。
假设transfer_cli.py中是这样使用boto3的:
# transfer_cli.py import boto3 def get_sftp_user_details(server_id, user_name): client = boto3.client('transfer') # ... 业务逻辑
修改测试代码中的@patch装饰器,改为patchtransfer_cli.boto3.client:
@patch("transfer_cli.boto3.client") # 改为patch目标模块内的boto3.client引用 def test_get_sftp_user_details(mock_boto_client): from transfer_cli import get_sftp_user_details # 其余代码保持不变 @patch("transfer_cli.boto3.client") def test_get_transfer_users(mock_boto_client): from transfer_cli import get_transfer_users # 其余代码保持不变
这样每次patch都会替换transfer_cli模块内实际使用的boto3.client引用,无论模块是否被缓存,都能保证当前测试使用的是最新的mock对象。
方案2:每次测试前重新加载模块
如果不想修改patch目标,可以在每个测试前强制重新加载transfer_cli模块,清除缓存:
import importlib # 新增导入 import transfer_cli # 提前导入模块 @patch("boto3.client") def test_get_sftp_user_details(mock_boto_client): importlib.reload(transfer_cli) # 测试前重新加载模块 from transfer_cli import get_sftp_user_details # 其余代码保持不变 @patch("boto3.client") def test_get_transfer_users(mock_boto_client): importlib.reload(transfer_cli) # 测试前重新加载模块 from transfer_cli import get_transfer_users # 其余代码保持不变
这种方法会让模块重新获取boto3.client的最新引用,但相比方案1代码更繁琐,且重新加载模块可能带来其他副作用,因此优先推荐方案1。
修改后批量运行所有测试,两个测试均应正常通过,mock的返回值会正确生效。
内容的提问来源于stack exchange,提问作者Paweł Kierski

