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