3

I am trying to use unittest.mock, but I am getting an error:

AttributeError: does not have the attribute 'get_pledge_frequency'

I have the following file structure:

pledges/views/
├── __init__.py
├── util.py
└── user_profile.py
pledges/tests/unit/profile
├── __init__.py
└── test_user.py

Inside pledges/views/__init___.py I have:

from .views import *
from .account import account
from .splash import splash
from .preferences import preferences
from .user_profile import user_profile

Inside, user_profile.py I have a function called user_profile which calls a function inside util.py called get_pledge_frequency as follows:

def user_profile(request, user_id):
    # some logic

    # !!!!!!!!!!!!!!!!
    a, b = get_pledge_frequency(parameter) # this is the function I want to mock

    # more logic

    return some_value

I have a test inside test_user.py as follows:

def test_name():
    with mock.patch(
        "pledges.views.user_profile.get_pledge_frequency"
    ) as get_pledge_frequency:
        get_pledge_frequency.return_value = ([], [])
        response = c.get(
            reverse("pledges:user_profile", kwargs={"user_id": user.id})
            ) # this calls the function user_profile inside pledges.user_profile

     # some asserts to verify functionality

I have checked other questions, but the answers do not cover when there is a function called as the module, and it is imported in the __init__ file.

So, is there any way to solve this problem? I have basically renamed the file user_profile.py to profile and then I have changed the tests to refer to the function inside this module, but I wonder if it is possible to keep the function and the module with the same name.

2
  • Why not simply change the filename? The ambiguity just seems to be asking for confusion. Commented Sep 14, 2018 at 3:40
  • 1
    @StephenRauch, this is what I did, but just for the sake of curiosity I wanted to know if it is somehow possible. Commented Sep 14, 2018 at 3:43

1 Answer 1

7

It turns out it is possible to mock a function called in a function inside a module with the same name. A small wrapper around unittest.mock.patch() can make that happen like so:

Code:

from unittest import mock
import importlib

def module_patch(*args):
    target = args[0]
    components = target.split('.')
    for i in range(len(components), 0, -1):
        try:
            # attempt to import the module
            imported = importlib.import_module('.'.join(components[:i]))

            # module was imported, let's use it in the patch
            patch = mock.patch(*args)
            patch.getter = lambda: imported
            patch.attribute = '.'.join(components[i:])
            return patch
        except Exception as exc:
            pass

    # did not find a module, just return the default mock
    return mock.patch(*args)

To use:

Instead of:

mock.patch("module.a.b")

you need:

module_patch("module.a.b")

How does this work?

Basic idea is to attempt module imports started with the longest possible module path towards the shortest path, and if an import is successful use that module as the patched object.

Test Code:

import module

print('module.a(): ', module.a())
print('module.b(): ', module.b())
print('--')

with module_patch("module.a.b") as module_a_b:
    module_a_b.return_value = 'new_b'
    print('module.a(): ', module.a())
    print('module.b(): ', module.b())

try:
    mock.patch("module.a.b").__enter__()
    assert False, "Attribute error was not raised, test case is broken"
except AttributeError:
    pass

Test files in module

# __init__.py
from .a import a
from .a import b


# a.py
def a():
    return b()

def b():
    return 'b'

Results:

module.a():  b
module.b():  b
--
module.a():  new_b
module.b():  b
Sign up to request clarification or add additional context in comments.

1 Comment

I have no words to describe how impressed I am. All my respect Sir.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.