使用 patch() 来模拟我没有明确导入的东西

Using patch() to mock something that I don't explicilty import

假设我有以下模块:

# src/myapp/utils.py
import thirdparty
from myapp.secrets import get_super_secret_stuff

def get_thirdparty_client() -> return thirdparty.Client:
    thirdparty.Client(**get_super_secret_stuff())
# src/myapp/things.py
from myapp.utils import get_thirdparty_client
from myapp.transformations import apply_fairydust, make_it_sparkle

def do_something(x):
    thirdparty_client = get_thirdparty_client()
    y = thidparty_client.query(apply_fairydust(x))
    return make_it_sparkle(y)

假设 myapp 是经过轻微测试的遗留代码,并且重构是不可能的。还假设(烦人地)thirdparty.Client 在其 __init__ 方法中执行非确定性网络 I/O。因此,我打算模拟 thirdparty.Client class 本身,以使这个 do_something 函数可测试。

还假设我必须使用 unittest 并且不能使用其他测试框架,如 Pytest。

unittest.mock 中的 patch 函数似乎是完成这项工作的正确工具。但是,我不确定如何将通常的警告应用到“使用补丁的地方”。

理想情况下,我想编写一个如下所示的测试:

# tests/test_myapp/test_things.py
from unittest import TestCase
from unittest.mock import patch

from myapp.things import do_something

def gen_test_pairs():
    # Generate pairs of expected inputs and outputs
    ...

class ThingTest(unittest.TestCase):
    @patch('????')
    def test_do_something(self, mock_thirdparty_client):
        for x, y in gen_test_pairs:
            with self.subTest(params={'x': x, 'y': y}):
                mock_thirdparty_client.query.return_value = y
                self.assertEqual(do_something(x), y)

我的问题是我不知道用什么来代替 ????,因为我从来没有真正在 src/myapp/things.py 中导入 thirdparty.Client

我考虑的选项:

  1. myapp.utils.thirdparty.Client 应用补丁,这使我的测试变得脆弱并依赖于实现细节。
  2. “打破规则”并在 thirdparty.Client 应用补丁。
  3. 在测试中导入get_thirdparty_client,在上面使用patch.object,并将其return_value设置为我单独创建的另一个MagicMock,这第二个mock就可以了参加 thirdparty.Client。这会导致更冗长的测试代码无法作为单个装饰器轻松应用。

None 这些选项听起来特别吸引人,但我不知道哪个被认为是最不糟糕的。

或者是否有其他我没有看到的选项可供选择?

正确答案是 2:将补丁应用于 thirdparty.Client

这是正确的,因为它实际上并没有违反“使用补丁的地方”规则。

该规则是描述性的,而不是字面意思。在这种情况下,thirdparty.Client 被认为在 thirdparty 模块中被“使用”。

Lisa Roach 在 2018 年 PyCon 演讲“揭秘补丁函数”中更详细地描述了这个概念。 YouTube 上提供了完整的录音:https://youtu.be/ww1UsGZV8fQ?t=537。这个特殊案例的解释从视频开始大约 9 分钟开始。