模拟全局变量
Posted
技术标签:
【中文标题】模拟全局变量【英文标题】:Mocking a global variable 【发布时间】:2016-11-15 02:54:27 【问题描述】:我一直在尝试为模块实现一些单元测试。一个名为 alphabet.py 的示例模块如下:
import database
def length_letters():
return len(letters)
def contains_letter(letter):
return True if letter in letters else False
letters = database.get('letters') # returns a list of letters
我想用我选择的一些值来模拟来自数据库的响应,但下面的代码似乎不起作用。
import unittests
import alphabet
from unittest.mock import patch
class TestAlphabet(unittest.TestCase):
@patch('alphabet.letters')
def setUp(self, mock_letters):
mock_letters.return_value = ['a', 'b', 'c']
def test_length_letters(self):
self.assertEqual(3, alphabet.length_letters())
def test_contains_letter(self):
self.assertTrue(alphabet.contains_letter('a'))
我见过很多例子,其中“补丁”应用于方法和类,但不应用于变量。我不想修补 database.get 方法,因为稍后我可能会再次使用它并使用不同的参数,所以我需要不同的响应。
我在这里做错了什么?
【问题讨论】:
【参考方案1】:您不需要使用模拟。只需导入模块并更改 setUp()
中的全局值即可:
import alphabet
class TestAlphabet(unittest.TestCase):
def setUp(self):
alphabet.letters = ['a', 'b', 'c']
【讨论】:
这种方法的一个不幸后果是,任何使用此模块级别变量的其他测试都将失败,除非您存储旧值并将其放回原处。模拟会为您解决这个问题。 您可以将alphabet.letters
的值设置回tearDown
函数中的值。
另外,由于setUp
的作用域是整个测试类,所以您只能将这个值用于letters
。 Will 在下面的回答让您可以为不同的测试用例制作多个模拟,最后它们会自行清理,因此不会有意外测试污染的风险。
这对于模拟来说绝对是不好的做法。测试之间共享的猴子补丁对象很容易导致奇怪的测试失败。
你也可以deepcopy
这个模块,从而解决这个问题【参考方案2】:
试试这个:
import unittests
import alphabet
from unittest import mock
class TestAlphabet(unittest.TestCase):
def setUp(self):
self.mock_letters = mock.patch.object(
alphabet, 'letters', return_value=['a', 'b', 'c']
)
def test_length_letters(self):
with self.mock_letters:
self.assertEqual(3, alphabet.length_letters())
def test_contains_letter(self):
with self.mock_letters:
self.assertTrue(alphabet.contains_letter('a'))
您需要在各个测试实际运行时应用模拟,而不仅仅是在setUp()
中。我们可以在setUp()
中创建模拟,然后使用with ...
上下文管理器应用它。
【讨论】:
这是我所要求的,但对于给出的示例,John 的回答似乎更好。我发现你的对其他情况很有用。谢谢。 没问题,很高兴为您提供帮助! 使用return_value
将导致字母成为可调用的MagicMock。但是我们并没有将字母作为函数来调用,我们也不需要 MagicMock 的任何属性,我们只是想替换值。所以我们应该直接传递值:mock.patch.object(alphabet, 'letters', ['a', 'b', 'c'])
如果您需要模拟多个值,这将如何工作?
@naught101 查看docs.python.org/3/library/unittest.mock.html#patch-multiple【参考方案3】:
我遇到了一个问题,我试图模拟出在任何函数或类之外使用的变量,这是有问题的,因为它们在你尝试模拟类的那一刻被使用,然后你才能模拟这些值。
我最终使用了一个环境变量。如果环境变量存在,则使用该值,否则使用应用程序默认值。这样我就可以在我的测试中设置环境变量的值了。
在我的测试中,我在导入类之前就有了这段代码
os.environ["PROFILER_LOG_PATH"] = "./"
在我的课堂上:
log_path = os.environ.get("PROFILER_LOG_PATH",config.LOG_PATH)
默认我的config.LOG_PATH
是/var/log/<my app name>
,但是现在运行测试时,日志路径设置为当前目录。这样您就不需要 root 访问权限来运行测试。
【讨论】:
理想情况下,您的测试在所有环境中都应该相同,无需任何额外配置。否则,它们可能会在您的本地机器上通过,但在其他地方会失败。 @Funkatic 是的,是的,但是您知道在导入时需要从另一个模块中模拟全局变量的方法吗? @fersarr 借鉴上面的例子,如果你根本不想调用database.get
,你需要先修补数据库模块,然后导入alphabet.py
。环境变量对于要加载的数据库名称等设置是可以的,但是基于变量动态加载一个或另一个数据库模块是自找麻烦。至少,它会让你的 linter 无用。回想起来,在导入时调用database.get
是个坏主意,应该避免。
我同意 ruth,其他答案将不起作用,因为只要您在测试文件顶部调用 import alphabet
,那么 database.get 就会在您模拟它之前运行。我一直无法找到解决此问题的方法。【参考方案4】:
可以按如下方式修补变量:
from mock import patch
@patch('module.variable', new_value)
例如:
import alphabet
from mock import patch
@patch('alphabet.letters', ['a', 'b', 'c'])
class TestAlphabet():
def test_length_letters(self):
assert 3 == alphabet.length_letters()
def test_contains_letter(self):
assert alphabet.contains_letter('a')
【讨论】:
在 Python 3.7 中也能正常工作 @ValeraManiuk 是常量所在的模块还是使用常量的代码所在的模块? @AlanH 我相信是前者。 此解决方案有效且干净。也可以只修补测试类中的一些测试 我遇到了类似的情况,我在数据库模块中有一个全局变量(导入)我尝试修补为 @patch('database.global_var', 'test') 但补丁不是工作任何帮助将不胜感激!以上是关于模拟全局变量的主要内容,如果未能解决你的问题,请参考以下文章