让 pytest 中的 monkeypatching 工作

Getting monkeypatching in pytest to work

我正在尝试使用 pytest 为 class 方法开发一个测试,该方法从字符串列表中随机选择一个字符串。

它基本上类似于下面的 givemeanumber 方法:

import os.path
from random import choice

class Bob(object):
  def getssh():
    return os.path.join(os.path.expanduser("~admin"), '.ssh')

  def givemeanumber():
    nos = [1, 2, 3, 4]
    chosen = choice(nos)
    return chosen

class Bob 中的第一个方法 getssh 只是 pytest docs

中的示例

我的生产代码从数据库中获取字符串列表,然后随机选择一个。所以我希望我的测试获取字符串,然后选择第一个字符串而不是随机选择。这样我就可以针对已知字符串进行测试。

根据我的阅读,我认为我需要使用 monkeypatching 来伪造随机化。

这是我目前所知道的

import os.path
from random import choice
from _pytest.monkeypatch import MonkeyPatch
from bob import Bob

class Testbob(object):
    monkeypatch = MonkeyPatch()

    def test_getssh(self):
        def mockreturn(path):
            return '/abc'
        Testbob.monkeypatch.setattr(os.path, 'expanduser', mockreturn)
        x = Bob.getssh()
        assert x == '/abc/.ssh'

    def test_givemeanumber(self):
        Testbob.monkeypatch.setattr('random.choice',  lambda x: x[0])
        z = Bob.givemeanumber()
        assert z == 1

第一个测试方法再次是 pytest 文档中的示例(我在测试中使用它时略有改编 class)。这很好用。

按照我希望使用的文档中的示例 Testbob.monkeypatch.setattr(random, 'choice', lambda x: x[0]) 但这会产生 NameError: name 'random' is not defined

如果我把它改成 Testbob.monkeypatch.setattr('random.choice', lambda x: x[0])

它更进一步但没有发生换出: AssertionError: assert 2 == 1

monkeypatching 是完成这项工作的正确工具吗? 如果是我错在哪里?

问题来自 Python 中变量名称的处理方式。与其他语言的主要区别在于,没有通过名称将值分配给变量;只有变量名与对象的绑定。

这是一个更大的话题,超出了这个问题的范围,但结果如下:

  1. 当您从模块 random 导入函数 choice 时,您将名称 choice 绑定到导入时存在的函数,并将此名称放在 bob 模块的本地名称空间中。

  2. 当您修补 random.choice 时,您将模块 random 的名称 choice 重新绑定到新的模拟对象。

  3. 但是,bob模块中已经导入的名称仍然指的是原来的函数。因为没有人修补它。函数本身没有修改,只是名称被替换了。

  4. 因此,Bob class 调用原始 random.choice 函数,而不是模拟函数。

要解决此问题,您可以采用以下两种方法之一(但不能同时采用两种方法,因为它们相互冲突):


A:总是用那个确切的全名调用 random.choice() 函数(即不是 choice)。当然,import random 之前(不是 from random import ...)——与 os.path.expanduser().

相同
# bob.py
import os.path
import random

class Bob(object):
  @classmethod
  def getssh(cls):
    return os.path.join(os.path.expanduser("~admin"), '.ssh')

  @classmethod
  def givemeanumber(cls):
    nos = [1, 2, 3, 4]
    chosen = random.choice(nos)   # <== !!! NOTE HERE !!!!
    return chosen

B:修补您调用的实际函数,在这种情况下是 bob.choice()(不是 random.choice())。

# test.py
import os.path
from _pytest.monkeypatch import MonkeyPatch
from bob import Bob

class Testbob(object):
    monkeypatch = MonkeyPatch()

    def test_givemeanumber(self):
        Testbob.monkeypatch.setattr('bob.choice',  lambda x: x[0])
        z = Bob.givemeanumber()
        assert z == 1

关于名称未知的原始错误 random:如果您想 patch(random, 'choice', ...),则必须 import random — 即,将名称 random 绑定到模块正在修补。

当您只执行 from random import choice 时,您将名称 choice 而不是 random 绑定到变量的本地命名空间。