让 pytest 中的猴子补丁工作

Pie*_*eau 1 python monkeypatching pytest

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

它看起来本质上类似于下面的 givemeannumber 方法:

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
Run Code Online (Sandbox Code Playgroud)

第一种方法,getssh,在 Bob 类中只是pytest 文档中的例子

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

根据我的阅读,我认为我需要使用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
Run Code Online (Sandbox Code Playgroud)

第一个测试方法再次是来自 pytest 文档的示例(在我在​​测试类中使用它时稍作调整)。这工作正常。

遵循我希望使用的文档中的示例, 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

猴子补丁是适合这项工作的工具吗?如果是我哪里出错了?

Ser*_*yev 5

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

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

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

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

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

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

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


A: 总是random.choice()用那个确切的全名来调用函数(即 not choice)。而且,当然,import random在 (not 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
Run Code Online (Sandbox Code Playgroud)

B:修补您调用的实际函数,bob.choice() 在那种情况下是 (not 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
Run Code Online (Sandbox Code Playgroud)

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

当你这样做只是from random import choice,你绑定的名称choice,而不是random在变量的本地命名空间。