
在使用 python `unittest.mock` 进行单元测试时,当模拟一个方法抛出异常并期望通过 `call_count` 验证其调用次数时,可能会遇到计数为零的现象。这通常是由于断言了类本身的 mock 对象,而非其返回的实例 mock 对象上的方法。本文将深入探讨这一问题的原因,并提供正确的断言方法,确保即使在异常场景下也能准确验证方法的调用。
在 Python 单元测试中,unittest.mock 是一个强大的工具,用于隔离被测代码与外部依赖。当我们需要模拟一个类时,通常会使用 patch 装饰器或上下文管理器。例如,with patch("module.Class") as mocked_class: 会将 module.Class 替换为一个 MagicMock 对象。
关键在于,这个 mocked_class 实际上是 Class 这个 类 的 Mock。当我们实例化这个类,例如 instance = mocked_class() 时,mocked_class 会返回另一个 MagicMock 对象,这个对象代表了 Class 的一个 实例。所有对 instance 的方法调用,实际上都是发生在 mocked_class.return_value 这个 Mock 对象上。
考虑以下场景:我们有一个 UploadService 类,其中 upload 方法内部会调用 Blob 类的实例方法 upload_from_string。我们希望测试当 upload_from_string 方法抛出异常时,UploadService 的行为。同时,我们还想验证 upload_from_string 确实被调用了一次。
原始代码示例:
立即学习“Python免费学习笔记(深入)”;
# upload_service.py
import json
import logging
# 假设 GoogleCloudError 和 Blob 是外部库的类
class GoogleCloudError(Exception):
pass
class Blob:
def __init__(self, name, bucket):
self.name = name
self.bucket = bucket
def upload_from_string(self, data, content_type):
print(f"Uploading data: {data} to {self.name} in {self.bucket}")
# 模拟实际的上传逻辑,这里简化
if "error" in data: # 示例:模拟特定数据触发异常
raise GoogleCloudError("Simulated upload error")
return True
class UploadService:
def __init__(self, name, gcs_bucket):
self.name = name
self.gcs_bucket = gcs_bucket
def upload(self, data):
try:
gcs_blob = Blob(self.name, self.gcs_bucket)
gcs_blob.upload_from_string(data=json.dumps(data), content_type="application/json")
return "Upload successful"
except GoogleCloudError as e:
logging.exception("Error uploading file")
return f"Upload failed: {e}"
# test_upload_service.py
import unittest
from unittest.mock import patch
from upload_service import UploadService, GoogleCloudError, Blob # 导入实际的Blob和GoogleCloudError
class TestUploadService(unittest.TestCase):
def test_upload_failure(self):
us = UploadService("my_file", "my_bucket")
with patch("upload_service.Blob") as mocked_blob_class:
# mocked_blob_class 是 Blob 类本身的 Mock
# gcs_blob 是 Blob 实例的 Mock
gcs_blob_instance = mocked_blob_class.return_value
gcs_blob_instance.upload_from_string.side_effect = GoogleCloudError("Google Cloud error")
result = us.upload({"status": "error"}) # 调用会触发异常
self.assertIn("Upload failed", result)
# 错误的断言方式:
# self.assertEqual(1, mocked_blob_class.upload_from_string.call_count) # ❌ 实际会是 0
在上述 test_upload_failure 示例中,如果尝试断言 mocked_blob_class.upload_from_string.call_count,测试将会失败,因为其值为 0。这是因为 upload_from_string 方法是作用在 Blob 的 实例 上,而不是 Blob 类 本身。当 us.upload() 内部调用 Blob(...) 时,mocked_blob_class 返回了一个 MagicMock 对象作为实例,即 gcs_blob_instance。真正被调用的方法是 gcs_blob_instance.upload_from_string,因此其调用计数应该记录在 gcs_blob_instance 上。
要正确验证 upload_from_string 方法的调用次数,我们应该断言在 mocked_blob_class.return_value(即 gcs_blob_instance)上的 upload_from_string 方法。
修改后的测试代码:
# test_upload_service.py (续)
class TestUploadService(unittest.TestCase):
def test_upload_failure_corrected(self):
us = UploadService("my_file", "my_bucket")
with patch("upload_service.Blob") as mocked_blob_class:
gcs_blob_instance = mocked_blob_class.return_value
gcs_blob_instance.upload_from_string.side_effect = GoogleCloudError("Google Cloud error")
result = us.upload({"status": "error"})
self.assertIn("Upload failed", result)
# 正确的断言方式:
self.assertEqual(1, gcs_blob_instance.upload_from_string.call_count)
# 或者直接通过 mocked_blob_class().upload_from_string.call_count 访问
self.assertEqual(1, mocked_blob_class().upload_from_string.call_count) # 这两种方式等价通过将断言目标从 mocked_blob_class.upload_from_string 更改为 gcs_blob_instance.upload_from_string (或 mocked_blob_class().upload_from_string),测试将如预期般通过。即使 side_effect 导致方法抛出异常,unittest.mock 仍然会正确记录该方法的调用。
当在 unittest.mock 中模拟一个方法抛出异常,并希望验证其调用次数时,核心在于正确识别并断言在接收到调用的 Mock 对象上。对于被 patch 的类,其实例方法的调用计数应在 类Mock.return_value.方法名.call_count 上进行验证,而不是 类Mock.方法名.call_count。理解这一区别是编写健壮、准确的 Python 单元测试的关键。
以上就是Python unittest.mock 中异常方法调用计数问题详解与解决方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号