20

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 ?

Community
  • 1
  • 1
Kévin Barré
  • 330
  • 1
  • 4
  • 11

4 Answers4

10

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.

Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
  • 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? – Avi Vajpeyi Sep 25 '20 at 02:50
  • 1
    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. – Don Kirkby Sep 25 '20 at 06:03
  • 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. – Avi Vajpeyi Sep 28 '20 at 23:46
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"]
        )
0

Here more simple method

orig_val=mymodule.MY_CONSTANT
mymodule.MY_CONSTANT=new_val

#some  test code code

mymodule.MY_CONSTANT=orig_val
  • 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](/help/how-to-answer). – Community Jul 10 '23 at 23:50
-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

Denys Synashko
  • 1,804
  • 3
  • 14
  • 13
  • 2
    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. – Kévin Barré May 03 '17 at 12:37
  • Then use 'with patch():' for your case. What more you need? Can't understand the problem, sorry. – Denys Synashko May 03 '17 at 13:48
  • In variant #3 you get the same result as #2 anyway , why you want something like #3 format? – Denys Synashko May 03 '17 at 13:56
  • 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. – Kévin Barré May 04 '17 at 09:35