pythonpytestpython-decoratorspytest-mock

List of function I want to Mock. Mocking function. Pytest


I have a list of functions that return Boolean values. It's all called like this:

for f in functions:
    if f():
        pass

I am writing tests and I want to mock the result of those functions

@pytest.fixture
def is_first_func_mock(mocker):
    mock = mocker.patch.object(SomeClass, 'first_func')
    return mock

and then somewhere in the test I pass this fixture and write is_first_func_mock.return_value = True or something like that

But since I'm not calling the function directly in the code, and since I indicated it above, the mock does not work.

I found a solution in Stackoverflow: I can call functions like that:

if getattr(funcs_module, func.__name__)():
    pass

But I don't like it and I don't want to change the loop

Update:

def some_name(fucntions: list[callable]):
    def inner(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            #some code
            for f in functions:
                if f():
                    return await func(*args, **kwargs)
            raise SomePermission

        return wrapper

    return inner

Solution

  • I haven't been able to find a solution, but here is a workaround:

    from functools import wraps
    
    def has_any_permissions(get_functions):
        def inner(func):
            @wraps(func)
            async def wrapper(*args, **kwargs):
                # some code
                funcs = get_functions()
                if any(f() for f in funcs):
                    return await func(*args, **kwargs)
                raise SomePermission()
    
            return wrapper
        return inner
    
    def is_admin():
        return False
    
    def is_users_moderator():
        return False
    
    def admin_and_users_moderator():
        return [is_admin, is_users_moderator]
    
    @has_any_permissions(admin_and_users_moderator)
    def about_user():
        return 'user'   # just for demonstration purposes
    

    I have had to introduce some indirection where the decorator does not bind the actual list of functions at decoration time, but instead supplies a function which, when called inside wrapper() gets the functions each time the target could be called.

    The corresponding unit test might look like this:

    import unittest
    from unittest.mock import patch
    
    from mockable_functions import about_user
    
    class Test(unittest.TestCase):
        def test_plain_function_raises(self):
            self.assertRaises(SomePermission, about_user)
    
        @patch('mockable_functions.is_admin')
        def test_func(self, is_admin_mock):
            is_admin_mock.return_value = True
            self.assertEqual(about_user(), 'user', 'about_user returns user')
    

    This unit test shows that about_user() raises an error when called normally, but when the is_admin function is patched, then the real function gets called (and returns a dummy value)