How to patch a constant in Python using a mock as function parameter
Asked Answered
F

4

23

I'm trying to understand the different ways to patch a constant in Python using mock.patch. My goal is to be able to use a variable defined in my Test class as the patching value for my constant.

I've found this question which explains how to patch a constant: How to patch a constant in python And this question which explains how to use self in patch: using self in python @patch decorator

But from this 2nd link, I cannot get the testTwo way (providing the mock as a function parameter) to work

Here is my simplified use case:

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

My guess is I shoudln't use return_value, because mock_MY_CONSTANT is not a function. So what attribute am I supposed to use to replace the value returned when the constant is called ?

Fillagree answered 16/2, 2017 at 13:19 Comment(0)
D
14

I think you're trying to learn about unit tests, mock objects, and how to replace the value of a constant in the code under test.

I'll start with your specific question about patching a constant, and then I'll describe a more general approach to replacing constant values.

Your specific question was about the difference between patch("mymodule.MY_CONSTANT", 3) and patch("mymodule.MY_CONSTANT"). According to the docs, the second parameter is new, and it contains the replacement value that will be patched in. If you leave it as the default, then a MagicMock object will be patched in. As you pointed out in your question, MagicMock.return_value works well for functions, but you're not calling MY_CONSTANT, so the return value never gets used.

My short answer to this question is, "Don't use MagicMock to replace a constant." If for some reason, you desperately wanted to, you could override the only thing you are calling on that constant, its __eq__() method. (I can't think of any scenario where this is a good idea.)

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)

Now for the more general question. I think the simplest approach is not to change the constant, but to provide a way to override the constant. Changing the constant just feels wrong to me, because it's called a constant. (Of course that's only a convention, because Python doesn't enforce constant values.)

Here's how I would handle what you're trying to do.

MY_CONSTANT = 5

def get_constant(override=MY_CONSTANT):
    return override

Then your regular code can just call get_constant(), and your test code can provide an override.

import unittest

import mymodule

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

This can become more painful as your code gets more complicated. If you have to pass that override through a bunch of layers, then it might not be worth it. However, maybe that's showing you a problem with your design that's making the code harder to test.

Denude answered 6/1, 2020 at 20:15 Comment(3)
So I dont want to create a variable wrapper for all my constants -- this can get kind of hairy! I tried your __eq__() method from test_get_constant_3. It passes a simple assertEqual test, but when i try to test a function that uses the mocked-constant, the constant appears as MagicMock name='MY_CONSTANT' id='140708674985744', rather than the mocked value... Any tips/updates on mocking constants?Lewis
I suggest you ask a new question, @AviVajpeyi, that describes what you're trying to do. Without any more details, I would suggest you try converting your constants to __init__() parameters with default values. Then your tests can pass in different values.Denude
Cool! I ended up making a function decorator that allows me to set constants before executing a test and then resetting them to their original values at the end of the test.Lewis
Q
0

I couldn't succeed using @patch("mymodule.MY_CONSTANT", 3), so I used the approach below. I know it is not the fancier approach but it is what worked for me.

I needed to mock the special method __str__, because the mocked constant becomes a MagickMock() object. And when it is used in a string, the __str__ will be called to parse the mock instance into a string. So mocking the __str__ I could force the intended value to the constant.

    @mock.patch("configuration_service.PROJECT_ROOT")
    def test_init(self, mock_root_constant):
        # arrange
        mocked_root_value = "<root>"
        mock_root_constant.__str__ = lambda *args: mocked_root_value

        # act
        ConfigurationService(dag_name)

        # assert
        mock_super_init.assert_called_once_with(
            searched_paths=[f"{mocked_root_value}/dags"]
        )
Quass answered 18/8, 2022 at 20:33 Comment(0)
E
-1

Here more simple method

orig_val=mymodule.MY_CONSTANT
mymodule.MY_CONSTANT=new_val

#some  test code code

mymodule.MY_CONSTANT=orig_val
Eshman answered 8/7, 2023 at 18:0 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Zaporozhye
A
-3

You can simpy assign mock value to constant before each assert:

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)

Some another example

# --- 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'

So no need to patch a constant at all

Antons answered 3/5, 2017 at 4:0 Comment(4)
First thanks for answering me ! Yes but in this case, it means that the value of mymodule.MY_CONSTANT is changed for all coming tests. That's why I want to use patch instead, so it is limited to the scope where I'm patching.Carotid
Then use 'with patch():' for your case. What more you need? Can't understand the problem, sorry.Antons
In variant #3 you get the same result as #2 anyway , why you want something like #3 format?Antons
Yes I know #2 works. I'm just curious and want to understand why I can"t have #3 to work for constants (while it works for functions). Sorry if this wasn't clear from my question.Carotid

© 2022 - 2025 — McMap. All rights reserved.