93

I've been trying to implement some unit tests for a module. An example module named alphabet.py is as follows:

import database

def length_letters():
    return len(letters)

def contains_letter(letter):
    return letter in letters


letters = database.get('letters')   # returns a list of letters

I'd like to mock the response from a database with some values of my choice, but the code below doesn't seem to work.

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

I have seen many examples in which 'patch' is applied to methods and classes, but not to variables. I prefer not to patch the method database.get because I may use it again with different parameters later on, so I would need a different response.

What am I doing wrong here?

DerMike
  • 15,594
  • 13
  • 50
  • 63
Funkatic
  • 1,113
  • 1
  • 8
  • 14

5 Answers5

129

Variables can be patched as follows:

from mock import patch
@patch('module.variable', new_value)    

For example:

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')
Tim Tisdall
  • 9,914
  • 3
  • 52
  • 82
Valera Maniuk
  • 1,627
  • 3
  • 11
  • 14
  • Works fine in Python 3.7 as well – RichVel Oct 18 '18 at 13:19
  • @ValeraManiuk Would that be the module the constant lives in or the module that the code using the constant lives in? – TheRealFakeNews Oct 23 '19 at 21:52
  • @AlanH I believe it's the former. – Imran Ariffin Nov 15 '19 at 13:55
  • 2
    This solution works and is clean. It is also possible to patch only some tests within the test class – Romain Jan 02 '20 at 16:18
  • 1
    I'm into a similar situation where I've a global variable in database module(imported) I tried patching as @patch('database.global_var', 'test') but the patch is not working any help would be appreciated! – Prashanna Oct 12 '20 at 16:45
  • 1
    for those struggling with why this isnt working with variables assigned above your classes/functions, it has to do with the way python loads imports vs patches. To overcome this, move the patch into your tests: `with mock.patch(module.variable, new_value): exceute_your_function_or_test` – lynkfox May 12 '22 at 20:24
  • Shouldn't the import look like from `unittest.mock import patch` when using the built-in library? – adrz Mar 22 '23 at 11:22
58

Try this:

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

You need to apply the mock while the individual tests are actually running, not just in setUp(). We can create the mock in setUp(), and apply it later with a with ... Context Manager.

Chiel
  • 1,865
  • 1
  • 11
  • 24
Will
  • 24,082
  • 14
  • 97
  • 108
  • 1
    This is what I was asking for, but John's answer seems better for the example given. I find yours useful for other cases though. Thank you. – Funkatic Jul 12 '16 at 22:55
  • 1
    No problem, glad to help! – Will Jul 12 '16 at 23:49
  • 32
    Using `return_value` will result in letters being a callable MagicMock. But we are not calling letters as a function, and we don't need any properties of MagicMock, we just want to replace the value. So instead we should pass the value directly: `mock.patch.object(alphabet, 'letters', ['a', 'b', 'c'])` – Geekfish Jun 07 '18 at 11:12
  • 1
    How does this work if you need to mock multiple values? – naught101 Jan 17 '19 at 01:29
  • @naught101 check out https://docs.python.org/3/library/unittest.mock.html#patch-multiple – user35915 Aug 25 '20 at 15:28
12

If you are using pytest-mock (see https://pypi.org/project/pytest-mock/), then all you need to do is use the built in fixture.

def test_my_function(mocker):
    # Mock the value of global variable `MY_NUMBER` as 10
    mocker.patch("path.to.file.MY_NUMBER", return_value=10)
    # rest of test...
Ali Sajjad
  • 3,589
  • 1
  • 28
  • 38
  • 1
    My problem with this arises when mocking a variable on which other classes and variables within the imported module are dependent. Ex., mock module.a to be 10, and module.b is defined on an if condition depending on module.a’s value. – preritdas Sep 02 '22 at 22:33
5

I ran into a problem where I was trying to mock out variables that were used outside of any function or class, which is problematic because they are used the moment you try to mock the class, before you can mock the values.

I ended up using an environment variable. If the environment variable exists, use that value, otherwise use the application default. This way I could set the environment variable value in my tests.

In my test, I had this code before the class was imported

os.environ["PROFILER_LOG_PATH"] = "./"

In my class:

log_path = os.environ.get("PROFILER_LOG_PATH",config.LOG_PATH)

By default my config.LOG_PATH is /var/log/<my app name>, but now when the test is running, the log path is set to the current directory. This way you don't need root access to run the tests.

  • 1
    Ideally, your tests should be identical on all environments, without any additional configuration. Otherwise they may pass on your local machine but fail somewhere else. – Funkatic Aug 15 '17 at 07:33
  • @Funkatic yes, true, but do you know of a way to mock globals from another module that that need to be defined during import time? – fersarr Nov 07 '19 at 10:47
  • @fersarr borrowing from the example above, if you don't want to call `database.get` at all, you would need to patch the database module first and then import `alphabet.py`. Environment variables are ok for settings such as the name of the db to be loaded, but dynamically loading one db module or another based on variables is asking for trouble. At the very least, it will make your linter useless. In retrospective, calling `database.get` on import is a bad idea and should be avoided. – Funkatic Jan 03 '20 at 14:00
  • I agree with ruth, the other answers would not work because as soon as you call `import alphabet` at the top of your test file then the database.get would run before you could mock it. I have not been able to find a solution to this. – Levi May 22 '20 at 12:25
-2

You don't need to use mock. Just import the module and alter the value of the global within setUp():

import alphabet

class TestAlphabet(unittest.TestCase): 
   def setUp(self):
        alphabet.letters = ['a', 'b', 'c']
John Gordon
  • 29,573
  • 7
  • 33
  • 58
  • 56
    An unfortunate consequence of this approach is that any other test who uses this module level variable will fail unless you store the old value and put it back. Mocking takes care of this for you. – Richard Løvehjerte Jul 25 '17 at 18:50
  • 1
    You can set the value of `alphabet.letters` back to what it was in the `tearDown` function. – tomas Sep 12 '17 at 09:12
  • 2
    Also, since `setUp` is scoped to the entire test class, you can only use this one value for `letters`. Will's answer below lets you make multiple mocks for different test cases, and they clean themselves up at the end so there's no risk of accidental test pollution. – raindrift Oct 24 '17 at 21:57
  • This is definitely bad practice for mocking. monkey-patching objects shared between tests can easily cause weird test failures. – Jordan Epstein Oct 01 '19 at 20:09
  • Also you might be able to `deepcopy` the module, thus getting over this issue – A T Jul 27 '20 at 05:21