模拟全局变量

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') 但补丁不是工作任何帮助将不胜感激!

以上是关于模拟全局变量的主要内容,如果未能解决你的问题,请参考以下文章

在 Jest 中模拟全局变量

如何在 mocha 中模拟全局变量?

如何在 JEST 测试中模拟全局 Vue.js 变量

是否可以在不使用全局变量的 PHP 5.2.x 中模拟闭包?

KEIL中全局变量,变量类型,LED灯,勿在中断延时

C#中在哪里声明全局变量啊,具体位置在哪儿,我是初学者。。。