【问题标题】:How to patch a constant in Python using a mock as function parameter如何使用模拟作为函数参数在 Python 中修补常量
【发布时间】:2017-07-05 15:13:37
【问题描述】:

我试图了解在 Python 中使用 mock.patch 修补常量的不同方法。 我的目标是能够使用我的 Test 类中定义的变量作为我的常量的修补值。

我发现这个问题解释了如何修补常量: How to patch a constant in python 这个问题解释了如何在补丁中使用 self : using self in python @patch decorator

但是从这个第二个链接,我无法让 testTwo 方式(提供模拟作为函数参数)工作

这是我的简化用例:

mymodule.py

MY_CONSTANT = 5

def get_constant():
    return MY_CONSTANT

test_mymodule.py

import unittest
from unittest.mock import patch

import mymodule

class Test(unittest.TestCase):

    #This works
    @patch("mymodule.MY_CONSTANT", 3)
    def test_get_constant_1(self):
        self.assertEqual(mymodule.get_constant(), 3)

    #This also works
    def test_get_constant_2(self):
        with patch("mymodule.MY_CONSTANT", 3):
            self.assertEqual(mymodule.get_constant(), 3)

    #But this doesn't
    @patch("mymodule.MY_CONSTANT")
    def test_get_constant_3(self, mock_MY_CONSTANT):
        mock_MY_CONSTANT.return_value = 3
        self.assertEqual(mymodule.get_constant(), 3)
        #AssertionError: <MagicMock name='MY_CONSTANT' id='64980808'> != 3

我的猜测是我不应该使用 return_value,因为 mock_MY_CONSTANT 不是函数。那么我应该使用什么属性来替换调用常量时返回的值呢?

【问题讨论】:

    标签: python unit-testing mocking constants patch


    【解决方案1】:

    我认为您正在尝试了解单元测试、模拟对象以及如何替换被测代码中的常量值。

    我将从您关于修补常量的具体问题开始,然后我将描述一种更通用的方法来替换常量值。

    您的具体问题是关于patch("mymodule.MY_CONSTANT", 3)patch("mymodule.MY_CONSTANT") 之间的区别。根据docs,第二个参数是new,它包含了将被修补的替换值。如果你保留它为默认值,那么MagicMock对象将被修补. 正如您在问题中指出的那样,MagicMock.return_value 适用于函数,但您没有调用MY_CONSTANT,因此永远不会使用返回值。

    我对这个问题的简短回答是,“不要使用 MagicMock 替换常量。”如果出于某种原因,您非常想这样做,您可以覆盖您在该常量上调用的唯一内容,它的 __eq__() 方法。 (我想不出这是个好主意的任何场景。)

    import unittest
    from unittest.mock import patch
    
    import mymodule
    
    class Test(unittest.TestCase):
    
        #This works
        @patch("mymodule.MY_CONSTANT", 3)
        def test_get_constant_1(self):
            self.assertEqual(mymodule.get_constant(), 3)
    
        #This also works
        def test_get_constant_2(self):
            with patch("mymodule.MY_CONSTANT", 3):
                self.assertEqual(mymodule.get_constant(), 3)
    
        #This now "works", but it's a horrible idea!
        @patch("mymodule.MY_CONSTANT")
        def test_get_constant_3(self, mock_MY_CONSTANT):
            mock_MY_CONSTANT.__eq__ = lambda self, other: other == 3
            self.assertEqual(mymodule.get_constant(), 3)
    

    现在是更一般的问题。我认为最简单的方法不是更改常量,而是提供一种覆盖常量的方法。改变常数对我来说感觉不对,因为它被称为常数。 (当然这只是一个约定,因为 Python 不强制执行常量值。)

    这就是我将如何处理你想要做的事情。

    MY_CONSTANT = 5
    
    def get_constant(override=MY_CONSTANT):
        return override
    

    那么您的常规代码可以只调用get_constant(),而您的测试代码可以提供覆盖。

    import unittest
    
    import mymodule
    
    class Test(unittest.TestCase):
        def test_get_constant(self):
            self.assertEqual(mymodule.get_constant(override=3), 3)
    

    随着您的代码变得越来越复杂,这可能会变得更加痛苦。如果您必须通过一堆层传递该覆盖,那么它可能不值得。但是,这可能表明您的设计存在问题,使代码更难测试。

    【讨论】:

    • 所以我不想为我的所有常量创建一个变量包装器——这可能有点麻烦!我从test_get_constant_3 尝试了你的__eq__() 方法。它通过了一个简单的 assertEqual 测试,但是当我尝试测试一个使用模拟常量的函数时,该常量显示为 MagicMock name='MY_CONSTANT' id='140708674985744',而不是模拟值...关于模拟常量的任何提示/更新?
    • 我建议你问一个新问题,@AviVajpeyi,它描述了你想要做什么。如果没有更多详细信息,我建议您尝试将常量转换为具有默认值的 __init__() 参数。然后你的测试可以传入不同的值。
    • 酷!我最终制作了一个函数装饰器,它允许我在执行测试之前设置常量,然后在测试结束时将它们重置为原始值。
    【解决方案2】:

    您可以在每个断言之前简单地将模拟值分配给常量:

    def test_get_constant_3(self):
        mymodule.MY_CONSTANT = 3
        self.assertEqual(mymodule.get_constant(), 3)
        mymodule.MY_CONSTANT = 7
        self.assertEqual(mymodule.get_constant(), 7)
    

    另一个例子

    # --- config.py ---
    
    class AppConf:
        APP_TIMEZONE = os.environ.get['APP_TIMEZONE']
    
    
    
    # --- my_mod.py ---
    
    from datetime import datetime
    from config import AppConf
    
    LOCAL_TZ = AppConf.APP_TIMEZONE
    
    def to_local_tz(dt_obj, tz):
        """Return datetime obj for specific timezone"""
        # some code here
        return local_dt_obj
    
    def get_local_time():
        return to_local_tz(datetime.utcnow(), LOCAL_TZ).strftime('%H:%M')
    
    
    
    # --- test_my_mod.py ---
    
    import my_mod
    
    class TestMyMod(unittest.TestCase):
        @patch('my_mod.datetime')
        def test_get_local_time(self, mock_dt):
            # Mock to 15:00 UTC
            mock_dt.utcnow.return_value = datetime(2017, 5, 3, 15)
    
            # Test with TZ 'Europe/Kiev'       +02:00 +03:00(DST)
            my_mod.LOCAL_TZ = 'Europe/Kiev'
            assert my_mod.get_local_time() == '18:00'
    
            # Test with TZ 'America/New_York'  -05:00 -04:00(DST)
            my_mod.LOCAL_TZ = 'America/New_York'
            assert my_mod.get_local_time() == '11:00'
    

    所以根本不需要修补常量

    【讨论】:

    • 首先感谢您回答我!是的,但在这种情况下,这意味着 mymodule.MY_CONSTANT 的值会针对所有即将到来的测试进行更改。这就是为什么我想改用补丁,所以它仅限于我正在修补的范围。
    • 然后根据你的情况使用'with patch():'。你还需要什么?无法理解问题,抱歉。
    • 在变体 #3 中,无论如何你得到的结果与 #2 相同,为什么你想要像 #3 格式的东西?
    • 是的,我知道 #2 有效。我只是好奇,想了解为什么我不能让#3 为常量工作(而它为函数工作)。对不起,如果我的问题不清楚。
    猜你喜欢
    • 1970-01-01
    • 2023-03-16
    • 2015-11-25
    • 2015-01-30
    • 2021-11-03
    • 1970-01-01
    • 2015-11-15
    • 2011-09-12
    • 1970-01-01
    相关资源
    最近更新 更多