nsfyn55
nsfyn55

Reputation: 15363

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

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?

Upvotes: 20

Views: 11256

Answers (3)

Andrew
Andrew

Reputation: 4418

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

Upvotes: 8

chepner
chepner

Reputation: 530970

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.

Upvotes: 24

minou
minou

Reputation: 16563

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

Upvotes: 7

Related Questions