什么时候应该使用依赖注入? 什么时候应该使用Mock?
![e5fe93cf0c2c06d0fac68602750013f7.png](https://i-blog.csdnimg.cn/blog_migrate/17581ca6aa1a718ee490a62c4998a2a8.jpeg)
> Photo by Helloquence on Unsplash.
我喜欢Python的一件事是它的测试设施。 当您需要模拟与外部依赖项的交互时,可以选择:
· 使用依赖项注入将依赖项替换为测试double。
· 使用Python的模拟库劫持实际的函数调用。
· 使用模拟响应对假服务器进行测试。
有了我们提供的所有这些测试策略,可能不清楚要使用哪种策略。 在本文中,我将讨论何时在依赖注入上选择模拟,反之亦然。
编码
让我们看一些代码来说明模拟和依赖注入之间的区别。 假设我正在测试调用库存服务的代码。 库存服务会跟踪用户持有的物品。 这是我们用来与广告资源服务进行交互的客户:
import requestsINVENTORY_SERVICE_URL = "http://inventory-service.com/api"def add_item(user_id, item_id): requests.post(f"{INVENTORY_SERVICE_URL}/{user_id}/items", json={ "item_id": item_id, })def get_items(user_id): r = requests.get(f"{INVENTORY_SERVICE_URL}/{user_id}/items") return r.json()
被测代码:
from src import inventoryBASIC_VOUCHER = 0SPECIAL_VOUCHER = 1VOUCHERS = {BASIC_VOUCHER, SPECIAL_VOUCHER}def send_vouchers(voucher_data): for user_id, voucher_id in voucher_data: inventory.add_item(user_id, voucher_id)def verify_has_voucher(user_id): items = inventory.get_items(user_id) for item_id in items: if item_id in VOUCHERS: return True return False
使用Mock方法的有效测试
尽管这不是有关如何使用模拟库的教程,但我将逐步通过代码来建立上下文:
from src import systemfrom unittest import mock@mock.patch("src.inventory.get_items")@mock.patch("src.inventory.add_item")def test_vouchers(add_mock, get_mock): mock_user = 0 data = [(mock_user, system.SPECIAL_VOUCHER)] get_mock.return_value = [system.SPECIAL_VOUCHER] system.send_vouchers(data) assert add_mock.called assert system.verify_has_voucher(mock_user) assert get_mock.called
· 我们不想调用库存服务,因此我们修补了get_items和add_item函数。
· 我们已将对库存服务的调用的返回值设置为我们期望的返回值。
使用依赖项注入的有效测试
在使用依赖注入之前,我们需要建立一个生产/测试环境以及一种选择在每个环境中运行的代码的方法。 让我们更改Client:
import osimport requestsfrom abc import ABC, abstractmethodclass Inventory(ABC): @abstractmethod def add_item(self, user_id, item_id): raise NotImplementedError @abstractmethod def get_items(self, user_id): raise NotImplementedErrorclass InventoryService(Inventory): INVENTORY_SERVICE_URL = "http://inventory-service.com/api" def add_item(self, user_id, item_id): requests.post(f"{self.INVENTORY_SERVICE_URL}/{user_id}/items", json={ "item_id": item_id, }) def get_items(self, user_id): r = requests.get(f"{self.INVENTORY_SERVICE_URL}/{user_id}/items") return r.json()class InventoryMock(Inventory): def __init__(self): self.data = {} def add_item(self, user_id, item_id): self.data[user_id] = self.data.get(user_id, []) + [item_id] def get_items(self, user_id): return self.data.get(user_id, [])client = Nonedef get_client(): global client if client is None: if os.environ.get("ENV") == "prod": client = InventoryService() else: client = InventoryMock() return client
免责声明:此代码足以说明该概念。 您需要为生产代码库进行更复杂的设置。
我们定义了两个具体的类。 如果我们不在产品环境中,则将使用InventoryMock类将数据保存在内存中。 不再需要使用模拟库。 我们的新测试如下所示:
from src import systemdef test_vouchers(): mock_user = 0 data = [(mock_user, system.SPECIAL_VOUCHER)] system.send_vouchers(data) assert system.verify_has_voucher(mock_user)
我应该使用什么?
两种策略都使我们能够在不调用清单服务的情况下测试代码。 在选择最合适的策略时,我会考虑以下几点:
范围/成本
根据您的代码状态,一种策略会比另一种便宜。 如果您需要模拟少数用例,则修补功能而不是创建模拟类可能更容易/更快。 如果您的代码库已经具有支持依赖项注入的基础结构和工具,那么编写简单的模拟类可能比打补丁更简单。
您还应该考虑所采用方法的未来后果。 如果您要模拟的交互方式可能会发生变化,请考虑选择修改最快的方法。
规模经济
依赖注入是扩展模拟方法的一种方式。 如果很多用例都依赖于您要模拟的交互,那么投资依赖注入是有意义的。 易于依赖注入的系统:
· 身份验证/授权服务。
· 负责分布式跟踪和跟踪指标的日志记录解决方案。
· 常用的基础结构,例如缓存和消息代理。
这些系统通常在整个代码库中使用:
![f970e499ef4bbc4cbd140cc4a5016000.png](https://i-blog.csdnimg.cn/blog_migrate/2f47142dd3162861d1f56ac29ef23979.jpeg)
这就是依赖注入的亮点-必须修补每个交互都会很痛苦。 如果有问题的系统跨多个存储库使用,那么您可以更进一步,并将依赖项注入类形式化到客户端库中。
总结思想
依赖注入和模拟都是测试外部依赖的值得推荐的方法。 依赖项注入需要更多的工作来设置,但对于高频使用来说它处于适当的位置。 模拟/修补方法是快速/容易的,但是随着依赖性使用的增加/更改,它开始变成技术债务。 还有两件事要牢记:
· 一致性:如果代码使用依赖性注入(或修补)来模拟交互,除非有明显的优势,否则没有理由偏离。
· 能力:如果工程师精通Python的模拟库,并且没有面向对象风格的依赖注入的经验(反之亦然),那么迁移到依赖注入的量化收益可能不会超过质量上的危害。
(本文翻译自Talha Malik的文章《Testing in Python: Dependency Injection vs. Mocking》,参考:https://medium.com/better-programming/testing-in-python-dependency-injection-vs-mocking-5e542783cb20)