在Python中进行测试:依赖注入与Mock

什么时候应该使用依赖注入? 什么时候应该使用Mock?

在Python中进行测试:依赖注入与Mock

> Photo by Helloquence on Unsplash.

我喜欢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

src

import

system

from

unittest

import

mock

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/<code>

· 我们不想调用库存服务,因此我们修补了get_items和add_item函数。

· 我们已将对库存服务的调用的返回值设置为我们期望的返回值。

使用依赖项注入的有效测试

在使用依赖注入之前,我们需要建立一个生产/测试环境以及一种选择在每个环境中运行的代码的方法。 让我们更改Client:

<code>

import

os

import

requests

from

abc

import

ABC, abstractmethod

class

Inventory

(ABC)

:

def

add_item

(self, user_id, item_id)

:

raise

NotImplementedError

def

get_items

(self, user_id)

:

raise

NotImplementedError

class

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

client

if

client

is

None

:

if

os.environ.get(

"ENV"

) ==

"prod"

: client = InventoryService()

else

: client = InventoryMock()

return

client/<code>

免责声明:此代码足以说明该概念。 您需要为生产代码库进行更复杂的设置。

我们定义了两个具体的类。 如果我们不在产品环境中,则将使用InventoryMock类将数据保存在内存中。 不再需要使用模拟库。 我们的新测试如下所示:

<code>

from

src

import

system

def

test_vouchers

()

:

mock_user =

0

data = [(mock_user, system.SPECIAL_VOUCHER)] system.send_vouchers(data)

assert

system.verify_has_voucher(mock_user)/<code>

我应该使用什么?

两种策略都使我们能够在不调用清单服务的情况下测试代码。 在选择最合适的策略时,我会考虑以下几点:

范围/成本

根据您的代码状态,一种策略会比另一种便宜。 如果您需要模拟少数用例,则修补功能而不是创建模拟类可能更容易/更快。 如果您的代码库已经具有支持依赖项注入的基础结构和工具,那么编写简单的模拟类可能比打补丁更简单。

您还应该考虑所采用方法的未来后果。 如果您要模拟的交互方式可能会发生变化,请考虑选择修改最快的方法。

规模经济

依赖注入是扩展模拟方法的一种方式。 如果很多用例都依赖于您要模拟的交互,那么投资依赖注入是有意义的。 易于依赖注入的系统:

· 身份验证/授权服务。

· 负责分布式跟踪和跟踪指标的日志记录解决方案。

· 常用的基础结构,例如缓存和消息代理。

这些系统通常在整个代码库中使用:

在Python中进行测试:依赖注入与Mock

这就是依赖注入的亮点-必须修补每个交互都会很痛苦。 如果有问题的系统跨多个存储库使用,那么您可以更进一步,并将依赖项注入类形式化到客户端库中。

总结思想

依赖注入和模拟都是测试外部依赖的值得推荐的方法。 依赖项注入需要更多的工作来设置,但对于高频使用来说它处于适当的位置。 模拟/修补方法是快速/容易的,但是随着依赖性使用的增加/更改,它开始变成技术债务。 还有两件事要牢记:

· 一致性:如果代码使用依赖性注入(或修补)来模拟交互,除非有明显的优势,否则没有理由偏离。

· 能力:如果工程师精通Python的模拟库,并且没有面向对象风格的依赖注入的经验(反之亦然),那么迁移到依赖注入的量化收益可能不会超过质量上的危害。

(本文翻译自Talha Malik的文章《Testing in Python: Dependency Injection vs. Mocking》,参考:https://medium.com/better-programming/testing-in-python-dependency-injection-vs-mocking-5e542783cb20)


分享到:


相關文章: