Python unittest mock: Is it possible to mock the value of a method's default arguments at test time?

StackOverflow https://stackoverflow.com//questions/24021491

  •  21-12-2019
  •  | 
  •  

Question

I have a method that accepts default arguments:

def build_url(endpoint, host=settings.DEFAULT_HOST):
    return '{}{}'.format(host, endpoint)

I have a test case that exercises this method:

class BuildUrlTestCase(TestCase):
    def test_build_url(self):
        """ If host and endpoint are supplied result should be 'host/endpoint' """

        result = build_url('/end', 'host')
        expected = 'host/end'

        self.assertEqual(result,expected)

     @patch('myapp.settings')
     def test_build_url_with_default(self, mock_settings):
        """ If only endpoint is supplied should default to settings"""
        mock_settings.DEFAULT_HOST = 'domain'

        result = build_url('/end')
        expected = 'domain/end'

        self.assertEqual(result,expected)

If I drop a debug point in build_url and inspect this attribute settings.DEFAULT_HOST returns the mocked value. However the test continues to fail and the assertion indicates host is assigned the value from my actual settings.py. I know this is because the host keyword argument is set at import time and my mock is not considered.

debugger

(Pdb) settings
<MagicMock name='settings' id='85761744'>                                                                                                                                                                                               
(Pdb) settings.DEFAULT_HOST
'domain'
(Pdb) host
'host-from-settings.com'                                                                                                                                                 

Is there a way to override this value at test time so that I can exercise the default path with a mocked settings object?

Was it helpful?

Solution

Functions store their parameter default values in the func_defaults attribute when the function is defined, so you can patch that. Something like

def test_build_url(self):
    """ If only endpoint is supplied should default to settings"""

    # Use `func_defaults` in Python2.x and `__defaults__` in Python3.x.
    with patch.object(build_url, 'func_defaults', ('domain',)):
      result = build_url('/end')
      expected = 'domain/end'

    self.assertEqual(result,expected)

I use patch.object as a context manager rather than a decorator to avoid the unnecessary patch object being passed as an argument to test_build_url.

OTHER TIPS

I applied the other answer to this question, but after the context manager, the patched function was not the same as before.

My patched function looks like this:

def f(foo=True):
    pass

In my test, I did this:

with patch.object(f, 'func_defaults', (False,)):

When calling f after (not in) the context manager, the default was completely gone rather than going back to the previous value. Calling f without arguments gave the error TypeError: f() takes exactly 1 argument (0 given)

Instead, I just did this before my test:

f.func_defaults = (False,)

And this after my test:

f.func_defaults = (True,)

An alternate way to do this: Use functools.partial to provide the "default" args you want. This isn't technically the same thing as overriding them; the call-ee sees an explicit arg, but the call-er doesn't have to provide it. That's close enough most of the time, and it does the Right Thing after the context manager exits:

# mymodule.py
def myfunction(arg=17):
    return arg

# test_mymodule.py
from functools import partial
from mock import patch

import mymodule

class TestMyModule(TestCase):
    def test_myfunc(self):
        patched = partial(mymodule.myfunction, arg=23)
        with patch('mymodule.myfunction', patched):
            self.assertEqual(23, mymodule.myfunction())  # Passes; default overridden
        self.assertEqual(17, mymodule.myfunction()) # Also passes; original default restored

I use this for overriding default config file locations when testing. Credit where due, I got the idea from Danilo Bargen here

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top