使用 unittest.mock 在 python 中修补 SMTP 客户端

Posted

技术标签:

【中文标题】使用 unittest.mock 在 python 中修补 SMTP 客户端【英文标题】:Patch SMTP client in python with unittest.mock 【发布时间】:2018-05-28 16:26:34 【问题描述】:

我想模拟 SMTP 客户端表单 smtplib 的生成。以下代码:

from smtplib import SMTP
from unittest.mock import patch

with patch('smtplib.SMTP') as smtp:
    print(SMTP, smtp)

返回

<class 'smtplib.SMTP'> <MagicMock name='SMTP' id='140024329860320'>

暗示补丁失败。

编辑:有趣的是,Monkey Patching as described here 给出了相同的结果。

import smtplib
from smtplib import SMTP
from unittest.mock import MagicMock

smtp = MagicMock()
smtplib.SMTP = smtp
print(SMTP, smtp)

【问题讨论】:

我原以为smtpSMTP 是相同的,因为基本上SMTPsmtplib.SMTP,它应该被修补。修补含义有效地替换为MagicMock 【参考方案1】:

我几乎不做任何补丁,但我相信你补丁要么太晚,要么是错误的事情。 SMTP 已经被导入,导致直接引用原始类——它不会再在smtplib 中查找。相反,您需要修补该引用。让我们用一个更现实的例子,你有module.pytest_module.py

module.py:

import smtplib
from smtplib import SMTP # Basically a local variable

def get_smtp_unqualified():
    return SMTP # Doing a lookup in this module

def get_smtp_qualified():
    return smtplib.SMTP # Doing a lookup in smtplib

test_module.py

import unittest
from unittest import patch
from module import get_smtp_unqualified, get_smtp_qualified

class ModuleTest(unittest.TestCase):
    def test_get_smtp_unqualified(self):
        with patch('module.SMTP') as smtp:
            self.assertIs(smtp, get_smtp_unqualified())

    def test_get_smtp_qualified_local(self):
        with patch('module.smtplib.SMTP') as smtp:
            self.assertIs(smtp, get_smtp_qualified())

    def test_get_smtp_qualified_global(self):
        with patch('smtplib.SMTP') as smtp:
            self.assertIs(smtp, get_smtp_qualified())

只要您在查找之前及时修补,它就会做您想做的事——3 次通过测试。最早的时间是在导入除unittest 之外的任何其他模块之前。那么这些模块还没有导入smtplib.SMTP。更多关于here。但是,当您的测试被拆分为多个模块时,这会变得很棘手。

修补本质上是脏的。你在搞乱别人的内部结构。为了让它发挥作用,你必须看看内部。如果内部发生变化,测试就会中断。这就是为什么您应该将其视为最后的手段并更喜欢不同的方式,例如依赖注入。这是一个完全不同的话题,但无论如何,不​​要依赖修补来防止消息流出——还要更改配置!

【讨论】:

这可能完全是另一个问题,但是您对如何在 Python 中使用 DI 完成此任务有任何指示吗? The community doesn't seem to agree how to do DI/IoC. @Benjamin 这是一个很大的话题。关键是保持简单,所以我同意没有 IoC 容器(“穷人的 DI”)。如果代码作为其工作的一部分需要发送电子邮件,不要让它实例化SMTP,而是传递一个配置的实例,在生产中是真实的,在单元测试时是假的。更好的是,抽象动作并限制接口,例如制作一个EmailSender 类,它只有一个采用地址、主题和正文的方法。生产版本将使用SMTP,但即使没有MagicMock,伪造版本也很容易制作。测试EmailSender 将不同于测试使用它的代码。 它是位于您的业务逻辑和世界其他部分之间的“基础设施”。使用集成测试(让它与一个虚拟的 SMTP 服务器对话)只测试那个小层,或者只要你触摸它就手动测试它。您将获得一棵要在根处组装的对象树,例如请求处理程序。那就是您进行实例化的地方,例如ThingThatSendsEmail(EmailSender(ConfigProvider()))。容器只是将手动工作转换为更具声明性的格式。 The Clean Architecture in Python 中最好地展示了另一个(也许更实用的)选项。 我必须补充一点,该演讲中 DI 的呈现方式非常具有误导性,例如在更高层级中必须通过越来越多的示例。可以通过更好地分配职责和使用具有通过__init__ 注入的依赖项而不是函数的对象来解决它。尽管如此,替代方案仍然有效。无论如何,这大约是我能装进几个 cmets 的量。我最重要的建议是不要将自己局限于 Python 世界或任何特定技术,而是专注于原则,然后找到你最喜欢的应用方式。

以上是关于使用 unittest.mock 在 python 中修补 SMTP 客户端的主要内容,如果未能解决你的问题,请参考以下文章

使用unittest.mock在python中修补SMTP客户端

Python 3:unittest.mock如何为特定输入指定不同的返回值?

Python unittest mock:是不是可以在测试时模拟 __init__ 的默认参数的值?

使用 unittest.mock.patch 测试 aiohttp 客户端

UnitTest, Mock整数类型返回NPE(null)报错

在 python 3.5 中模拟异步调用