18

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?

nsfyn55
  • 14,875
  • 8
  • 50
  • 77
  • Make sure you are patching the correct instance of `myapp`. Where is `build_url` defined? You may need something like `@patch('module.myapp.settings')` rather than `@patch(myapp.settings)`. – chepner Jun 03 '14 at 17:41
  • Yep, a quick of settings in the debugger shows that it is being patched correctly(`settings = – nsfyn55 Jun 03 '14 at 17:54
  • Oh, right. The default is set when the function is defined (the same reason `def foo(x=[])` doesn't give you a fresh empty list every time you call `foo()`). Which leads to a possible answer... – chepner Jun 03 '14 at 18:28

3 Answers3

23

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.

Cloud
  • 2,859
  • 2
  • 20
  • 23
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Will this replace `build_url` with a `Mock`? That is the method I want to test. – nsfyn55 Jun 03 '14 at 18:49
  • Whoops never mind. Worked like a champ! Quick edit you don't want to patch the string `'build_url'` rather the assigned variable. – nsfyn55 Jun 03 '14 at 18:56
  • 1
    This answer looked great at first but tripped me up for the reason listed in the answer by [Jeff O'Neill](https://stackoverflow.com/a/42179486/496445) – jdi Mar 28 '18 at 02:35
  • 1
    Oh! That's so close to what I was looking for. Anyone know how you can do similar with the `__init__` method? Similar to @nsfyn55 I want to change a default value but from the constructor :-/ I tried @chepner cleaver trick but I think It can't be done on a static method. – Rastikan May 29 '18 at 16:53
  • 1
    When I try to use a path instead of a function, `@patch.object('some_module.my_function', '__defaults__', (22,))` I get `AttributeError: some_model.my_function does not have the attribute '__defaults__'` – Boris Verkhovskiy Mar 17 '20 at 17:48
  • 1
    `patch.object` takes a reference to the actual object, not a qualified name as a `str`, as the first argument. `@patch.object(some_module.my_function, '__defaults__', (22,))`. – chepner Mar 17 '20 at 18:01
  • @chepner what if I want to patch the default arguments of a function that is imported in the function that I'm testing? I'm testing a function `my_module.main_function` which also defines a `my_module._minor_function` and I want to change the default arguments of `_minor_function` but use `main_function` in my test. The test is in a separate file of course. – Boris Verkhovskiy Mar 17 '20 at 18:07
  • Please post a new question where you can show your use case more clearly. – chepner Mar 17 '20 at 18:11
  • @chepner https://stackoverflow.com/questions/60728285/how-do-i-mock-the-default-arguments-of-a-function-that-is-used-by-the-function-i – Boris Verkhovskiy Mar 17 '20 at 18:30
8

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

Andrew
  • 4,058
  • 4
  • 25
  • 37
  • I'm not sure this covers my core use case. I am looking to test that this function works as expected with a default parameter vs. a provided kwarg. If I just simulate providing the `kwarg`its not really producing anything of value. I could just create a test that supplies `arg=23` and achieve the same net effect. – nsfyn55 Mar 04 '19 at 19:45
  • This is pretty nifty. – fabian789 Jan 06 '22 at 13:03
  • I prefer this answer to the others: `__defaults__` is bound to change as the signature of the function changes and it can be easy to miss that. Also, the code is more obvious in this answer. – Le Frite Mar 28 '23 at 10:11
7

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,)
new name
  • 15,861
  • 19
  • 68
  • 114
  • Had a similar issue, thanks for posting! Would you say this is a bug? Seems odd the defaults are returned to the original? – Josmoor98 Jul 08 '22 at 12:01