【问题标题】: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 方法,因为稍后我可能会再次使用它并使用不同的参数,所以我需要不同的响应。

我在这里做错了什么?

【问题讨论】:

    标签: python unit-testing mocking patch


    【解决方案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'])
    • 如果您需要模拟多个值,这将如何工作?
    【解决方案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') 但补丁不是工作任何帮助将不胜感激!
    猜你喜欢
    • 1970-01-01
    • 2017-03-19
    • 1970-01-01
    • 2018-11-21
    • 1970-01-01
    • 2020-07-21
    • 2019-04-07
    • 1970-01-01
    相关资源
    最近更新 更多