How to patch a constant in python
Asked Answered
C

4

55

I have two different modules in my project. One is a config file which contains

LOGGING_ACTIVATED = False

This constant is used in the second module (lets call it main) like the following:

if LOGGING_ACTIVATED:
    amqp_connector = Connector()

In my test class for the main module i would like to patch this constant with the value

True

Unfortunately the following doesn't work

@patch("config.LOGGING_ACTIVATED", True)

nor does this work:

@patch.object("config.LOGGING_ACTIVATED", True)

Does anybody know how to patch a constant from different modules?

Cotta answered 2/12, 2014 at 15:20 Comment(1)
This did work for me... from mock import patch @patch("location.of.file.and.CONSTANT", mockValue) Halfandhalf
K
64

If the if LOGGING_ACTIVATED: test happens at the module level, you need to make sure that that module is not yet imported first. Module-level code runs just once (the first time the module is imported anywhere), you cannot test code that won't run again.

If the test is in a function, note that the global name used is LOGGING_ACTIVATED, not config.LOGGING_ACTIVATED. As such you need to patch out main.LOGGING_ACTIVATED here:

@patch("main.LOGGING_ACTIVATED", True)

as that's the actual reference you wanted to replace.

Also see the Where to patch section of the mock documentation.

You should consider refactoring module-level code to something more testable. Although you can force a reload of module code by deleting the module object from the sys.modules mapping, it is plain cleaner to move code you want to be testable into a function.

So if your code now looks something like this:

if LOGGING_ACTIVATED:
    amqp_connector = Connector()

consider using a function instead:

def main():
    global amqp_connector
    if LOGGING_ACTIVATED:
        amqp_connector = Connector()

main()

or produce an object with attributes even.

Kiblah answered 2/12, 2014 at 15:25 Comment(6)
Beat me to it, I was busy trying to figure out whether patch() is capable of patching the __main__ module, just in case that is what was meant by "call it main".Hotspur
@SteveJessop: for the record: yes it is. __main__ is just another module as far as Python is concerned so patch('__main__.somename', somevalue) works.Kiblah
thanks for the fast answer. The if statement is indeed on module level. And in my test class I import the module. So there is no chance to rewrite it for single test methods?Cotta
@d.a.d.a: You can force a reload of the module by deleting it from sys.modules. if 'main' in sys.modules: del sys.modules['main'].Kiblah
@d.a.d.a: however, I'd refactor the module to use a function instead, ran from the top-level with a single call. That way you can test the function instead.Kiblah
thanks for your help it got me to the right thought. Hence I just needed the variable initialized within the if statement to be patched I did that in the end. Could have seen that from the beginningCotta
T
26

The problem you are facing is that you are mocking where it is defined, and you should patch where is it used.

Mock an item where it is used, not where it came from.

I leave you some example code, so you can catch the idea.

project1/constants.py

INPUT_DIRECTORY="/input_folder"

project1/module1.py

from project1.constants import INPUT_DIRECTORY
import os

def clean_directories():
    for filename in os.listdir(INPUT_DIRECTORY):
        filepath = os.path.join(directory, filename)
        os.remove(filepath)

project1/tests/test_module1.py

import mock, pytest

def test_clean_directories(tmpdir_factory):
    """Test that folders supposed to be emptied, are effectively emptied"""

    # Mock folder and one file in it
    in_folder = tmpdir_factory.mktemp("in")
    in_file = in_folder.join("name2.json")
    in_file.write("{'asd': 3}")

    # Check there is one file in the folder
    assert len([name for name in os.listdir(in_folder.strpath) if os.path.isfile(os.path.join(path, name))]) == 1

    # As this folder is not a parameter of the function, mock it.
    with mock.patch('project1.module1.INPUT_DIRECTORY', in_folder.strpath):
        clean_directories()

    # Check there is no file in the folder
    assert len([name for name in os.listdir(in_folder.strpath) if os.path.isfile(os.path.join(path, name))]) == 0

So the important line would be this one:

with mock.patch('project1.module1.INPUT_DIRECTORY', in_folder.strpath):

See, value is mocked where it is used, and not in constants.py (where it is defined)

Troika answered 17/9, 2020 at 18:34 Comment(5)
My god, the remark about "where it is used" has saved me so much time. Thanks!Talipes
And that is true for everything you mock @Dr_Zaszuś =) .. when I learned that testing mantra, it was the moment I knew how to testTroika
It is a good mantra... but supposing you want to make sure that test code always puts output in a tmpdir, never in the "real" location. Then you'd preferably want to find a universal fixture which does that for all tests. Because you don't necessarily always know whether a test is going to cause app code to produce output in these sorts of situations.Grandsire
@mikerodent can you provide an example with what you mean? I do not know if patching a variable hast to be with a fixture. Or did not fully understand what you meant.Troika
Added an answer. No, there's no obligation about using a fixture, it's just a suggestion.Grandsire
U
7

Found this thread while having a similar issue, what worked for me:

from unittest import mock

@mock.patch('<name_of_module>.<name_of_file>.<CONSTANT_NAME>', <value_to_replace_with>)
class Test_<name_of_test_suit>(unittest.TestCase):

  def test_<name_of_test>(self):
    <test_body>

Keep in mind that you would need and __init__.py to treat directories containing files as packages. https://docs.python.org/3/tutorial/modules.html#:~:text=The%20__init__.py,on%20the%20module%20search%20path.

P.S. Have a look at https://chase-seibert.github.io/blog/2015/06/25/python-mocking-cookbook.html

Unproductive answered 3/11, 2020 at 13:53 Comment(2)
This will return the mock instead.Fernery
It sounds like you are correct, but if I want to test the value of the constant purely, not that much work is needed? I am more interested in how the rest of the code behaves under the condition that the constant is set to something different. Thank you for chipping in :)Unproductive
G
0

A universal fixture (one run for every test) might be a solution for this. It is limited by the things which Martijn Pieters mentions in his answer.

To mock out for the purposes of any and all tests, put this in conftest.py. It presupposes that DOCS_ROOT_DIR_PATH_STR is a path where you might have some documents for processing (i.e. input), or where you output something. Typically in a test you might want to use a set of special test documents (if inputting) or you might want to avoid spawning loads of documents to a real location (if outputting):

@pytest.fixture(scope='function', autouse=True)
def universal_fixture(tmpdir):
    with mock.patch('src.core.__main__.DOCS_ROOT_DIR_PATH_STR', str(tmpdir)):         
        yield

By "logging" to a text file I was able to ascertain that this works for everything except the initial import of the files: this importing happens before conftest.py is run. So the constant value is set in src.core.__main__ even before this fixture runs. However, that shouldn't matter too much because by the time you get to running any function/method the patch will have taken effect.

NB if it is used in the actual test function, tmpdir uses the same path as the tmpdir in this universal fixture.

Grandsire answered 24/8, 2023 at 17:53 Comment(1)
I did not get what this has to do with patching a constant. Can you place an example with code?Troika

© 2022 - 2025 — McMap. All rights reserved.