@patch() не приводит к ожидаемому поведению внутри декоратора

#python #unit-testing #mocking

#python #модульное тестирование #издевательство

Вопрос:

Я тестирую функцию просмотра в Flask, которая требует аутентификации. Я хочу убрать эффект аутентификации, и поэтому я написал тест следующим образом:

 @patch('auth0_.decode_jwt')
@patch('auth0_.get_token_auth_header')
def test_list_cards_endpoint(self, get_token_header, decode_jwt):
    decode_jwt.return_value = {
        'sub': 'auth0|123',
    }
    headers = {
        'Authorization': 'Bearer 123'
    }
    with current_app.test_request_context('/pay/list-cards', headers=headers):
        list_cards_endpoint()
 

Который работает совершенно нормально. Затем я хотел абстрагировать приведенный выше код, чтобы я мог повторно использовать все его тесты, и поэтому я превратил его в декоратор:

 def fake_auth0(f):
    @patch('auth0_.decode_jwt')
    @patch('auth0_.get_token_auth_header')

    def decorated(get_token_header, decode_jwt, *args, **kwargs):
        decode_jwt.return_value = {
            'sub': 'auth0|123',
        }
        headers = {
            'Authorization': 'Bearer 123'
        }
        with current_app.test_request_context('/pay/list-cards', headers=headers):
            return f(*args, **kwargs)

    return decorated
 

И попытался вызвать его:

 @fake_auth0
def test_list_cards_endpoint(self):
    list_cards_endpoint()
 

И я получаю сообщение об ошибке, суть которого заключается в том, что @patch операторы работают некорректно. Более конкретно, они не присваивают значение return_value быть тем, что я хочу, и вместо этого я просто получаю <MagicMock name='decode_jwt().__getitem__()' id='4540406656'> в качестве возвращаемого значения decode_jwt .

Так что это похоже на — макет работает, но возвращаемое значение — нет.

Почему это может быть?

Комментарии:

1. @gilch не могли бы вы уточнить немного больше? Каковы последствия того, что вы сказали? Любые ссылки, которыми вы могли бы поделиться, чтобы я мог прочитать?

2. Вы пытались использовать версию диспетчера контекста patch (например with patch(...) as get_token_header: ) вместо декораторов? Я не уверен, правильно ли обрабатываются декораторы, если внутри другого декоратора.

3. @MrBeanBremen оставлял это в качестве последнего средства — если из этого потока не выйдет ничего хорошего, тогда придется. Хорошее предложение!

Ответ №1:

Вам не хватает self параметра.

Функция не является методом во время определения класса, когда применяется декоратор. self Не привязывается, пока вы не получите к нему доступ как к attr с точкой, которая запускает его дескриптор.

И @patch передает свой объект в качестве последнего аргумента, а не первого, поэтому *args подобное использование не сработает.

Вы могли бы попробовать вообще не передавать их в качестве аргументов:

 def fake_auth0(f):
    get_token_header = MagicMock()
    decode_jwt = MagicMock()
    decode_jwt.return_value = {
        'sub': 'auth0|123',
    }

    @patch('auth0_.decode_jwt', decode_jwt)
    @patch('auth0_.get_token_auth_header', get_token_header)
    def decorated(*args, **kwargs):    
        headers = {
            'Authorization': 'Bearer 123'
        }
        with current_app.test_request_context('/pay/list-cards', headers=headers):
            return f(*args, **kwargs)

    return decorated
 

Или, в качестве альтернативы, извлеките их из *args , прежде чем передавать их дальше:

 
def fake_auth0(f):
    @patch('auth0_.decode_jwt', return_value={'sub': 'auth0|123'})
    @patch('auth0_.get_token_auth_header')
    def decorated(*args, **kwargs):
        *args, get_token_auth_header, decode_jwt = args
        headers = {
            'Authorization': 'Bearer 123'
        }
        with current_app.test_request_context('/pay/list-cards', headers=headers):
            return f(*args, **kwargs)

    return decorated
 

Ответ №2:

Приведенное выше предложение @gilch работает над устранением исходной ошибки, но затем я начинаю сталкиваться с новыми, а именно с этим:

 AttributeError: Attributes cannot start with 'assert' or 'assret'
 

Я полагаю, это связано с тем, что декоратор определен вне класса, но его определение внутри класса также сопряжено с проблемами.

В общем, я отказался от декораторов и просто написал менеджер контекста:

 @contextlib.contextmanager
def auth0_faker(url):
    with patch('auth0_.decode_jwt') as mock_decode_jwt:
        with patch('auth0_.get_token_auth_header') as mock_auth_header:
            mock_decode_jwt.return_value = {
                'sub': 'auth0|123',
            }
            headers = {
                'Authorization': 'Bearer 123'
            }
            with current_app.test_request_context(url, headers=headers):
                yield
 

Я нахожу это намного проще для чтения и понимания.