让 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 中变量名称的处理方式。与其他语言的主要区别在于,没有通过名称将值分配给变量;只有变量名与对象的绑定。
这是一个更大的话题,超出了这个问题的范围,但结果如下:
当您从模块 random
导入函数 choice
时,您将名称 choice
绑定到导入时存在的函数,并将此名称放在 bob
模块的本地名称空间中。
当您修补 random.choice
时,您将模块 random
的名称 choice
重新绑定到新的模拟对象。
但是,bob
模块中已经导入的名称仍然指的是原来的函数。因为没有人修补它。函数本身没有修改,只是名称被替换了。
因此,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
绑定到变量的本地命名空间。
我正在尝试使用 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 中变量名称的处理方式。与其他语言的主要区别在于,没有通过名称将值分配给变量;只有变量名与对象的绑定。
这是一个更大的话题,超出了这个问题的范围,但结果如下:
当您从模块
random
导入函数choice
时,您将名称choice
绑定到导入时存在的函数,并将此名称放在bob
模块的本地名称空间中。当您修补
random.choice
时,您将模块random
的名称choice
重新绑定到新的模拟对象。但是,
bob
模块中已经导入的名称仍然指的是原来的函数。因为没有人修补它。函数本身没有修改,只是名称被替换了。因此,
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
绑定到变量的本地命名空间。