pytest-monkeypatch декоратор (не использует mock/patch)

Я пишу несколько тестов, используя pytest с приспособлением monkeypatch. Следуя правилам, я импортирую классы и методы для имитации из модуля, в котором они используются, а не из источника.

Приложение, для которого я пишу тесты, представляет собой приложение Google App Engine, использующее стандартную среду. Таким образом, я должен использовать python 2.7, фактическая версия, которую я использую, — 2.7.15, а версия pytest — 3.5.0.

До сих пор все работало хорошо, но я столкнулся с проблемой при попытке смоделировать функцию декоратора.

Начиная сверху. В файле py с именем decorators.py содержатся все декораторы авторизации, включая декоратор, который я хочу смоделировать. Рассматриваемый декоратор является функцией модуля, а не частью класса.

def user_login_required(handler):
    def is_authenticated(self, *args, **kwargs):
        u = self.auth.get_user_by_session()
        if u.access == '' or u.access is None:
            # return the response
            self.redirect('/admin', permanent=True)
        else:
            return handler(self, *args, **kwargs)
    return is_authenticated

Декоратор применяется к функции веб-запроса. Простой пример в файле с именем UserDetails.py в папке с именем обработчики (handlers.UserDetails)

from decorators import user_login_required

class UserDetailsHandler(BaseHandler):
    @user_login_required
    def get(self):
        # Do web stuff, return html, etc

В тестовом модуле я настраиваю тест следующим образом:

from handlers.UserDetails import user_login_required

@pytest.mark.parametrize('params', get_params, ids=get_ids)
def test_post(self, params, monkeypatch):

    monkeypatch.setattr(user_login_required, mock_user_login_required_func)

Проблема в том, что monkeypatch не позволяет мне указать одну функцию в качестве цели. Он хочет, чтобы целью был класс, за которым следует имя метода, которое должно быть заменено, а затем фиктивный метод....

monkeypatch.setattr(WouldBeClass, "user_login_required", mock_user_login_required_func)

Я попытался настроить код, чтобы увидеть, смогу ли я обойти его, изменив способ импорта и использования декоратора следующим образом:

import decorators

class UserDetailsHandler(BaseHandler):
    @decorators.user_login_required
    def get(self):
        # Do web stuff, return html, etc

Затем в тесте я пытаюсь исправить имя функции вот так.....

from handlers.UserDetails import decorators

@pytest.mark.parametrize('params', get_params, ids=get_ids)
def test_post(self, params, monkeypatch):

    monkeypatch.setattr(decorators, "user_login_required" , mock_user_login_required_func)

Хотя этот код не выдает никаких ошибок, когда я прохожу тест, код никогда не входит в mock_user_login_required_func. Он всегда входит в живой декоратор.

Что я делаю неправильно? Это проблема с попыткой патчить декораторы в целом, или отдельные функции в модулях не могут быть исправлены?


person solo1977    schedule 22.07.2018    source источник
comment
Типичная проблема с исправлением декораторов заключается в том, что как только модуль импортируется, декоратор запускается, модифицирует функцию и больше никогда не запускается для этой функции. Последующее исправление не дает никакого эффекта.   -  person Klaus D.    schedule 22.07.2018
comment
Понятное объяснение, спасибо, Клаус. Похоже, мне нужно сделать что-то непосредственно в декораторе, чтобы определить, выполняется ли тест. У меня есть идея... буду исследовать дальше.   -  person solo1977    schedule 22.07.2018
comment
Возможный дубликат Могу ли я исправить декоратор Python до он обертывает функцию?   -  person hoefling    schedule 22.07.2018


Ответы (3)


Похоже, что быстрый ответ здесь — просто переместить импорт Handler так, чтобы он происходил после исправления. Декоратор и декорированные функции должны быть в отдельных модулях, чтобы Python не выполнял декоратор до того, как вы его исправите.

from decorators import user_login_required

@pytest.mark.parametrize('params', get_params, ids=get_ids)
def test_post(self, params, monkeypatch):

    monkeypatch.setattr(decorators, "user_login_required" , mock_user_login_required_func)
    from handlers.UserDetails import UserDetailsHandler

Возможно, вам будет проще это сделать, используя функцию patch из встроенного модуля unittest.mock.

person soundstripe    schedule 22.07.2018
comment
Отмечая это как принятый ответ, поскольку он решает проблему. - person solo1977; 23.07.2018

Из-за упомянутых здесь ошибок импорта/модификации я решил не пытаться использовать насмешки для этого конкретного декоратора.

На данный момент я создал приспособление для установки переменной среды:

@pytest.fixture()
def enable_fake_auth():
    """ Sets the "enable_fake_auth"  then deletes after use"""
    import os
    os.environ["enable_fake_auth"] = "true"
    yield
    del os.environ["enable_fake_auth"]

Затем в декораторе я изменил метод is_authenticated:

def is_authenticated(self, *args, **kwargs):
    import os
    env = os.getenv('enable_fake_auth')
    if env:
        return handler(self, *args, **kwargs)
    else:
        # get user from session
        u = self.auth.get_user_by_session()
        if u:
            access = u.get("access", None)
            if access == '' or access is None:
                # return the response
                self.redirect('/admin', permanent=True)
            else:
                return handler(self, *args, **kwargs)
        else:
            self.redirect('/admin?returnPath=' + self.request.path, permanent=True)

return is_authenticated

Это не отвечает на мой первоначальный вопрос, но я разместил свое решение здесь на случай, если оно может помочь кому-то еще. Как указал Хофлинг, подобное изменение производственного кода обычно является плохой идеей, поэтому используйте его на свой страх и риск!

Исходное решение, которое у меня было до этого, не изменяло и не издевалось над каким-либо кодом. Это включало создание поддельного безопасного файла cookie, а затем отправку его в заголовках тестового запроса. Это заставит вызов self.auth.get_user_by_session() вернуть действительный объект с набором доступа. Я могу вернуться к этому.

person solo1977    schedule 22.07.2018
comment
Предупреждаю будущих читателей: изменение поведения рабочего кода в тестовом режиме — плохая идея и очень опасный путь. Пожалуйста, никогда так не делай. - person hoefling; 22.07.2018

У меня была аналогичная проблема, и я решил ее, используя патч в приборе, чтобы исправить код, на который отложил декоратор. Чтобы дать некоторый контекст, у меня было представление о проекте Django, который использовал декоратор в функции представления для обеспечения аутентификации. Типа что-то вроде:

# myproject/myview.py

@user_authenticated("some_arg")
def my_view():
    ... normal view code ...

Код для user_authenticated жил в отдельном файле:

# myproject/auth.py

def user_authenticated(argument):
    ... code for the decorator at some point had a call to:
    actual_auth_logic()
    
    
def actual_auth_logic():
    ... the actual logic around validating auth ...

Для проверки я написал что-то вроде:

import pytest
from unittest.mock import patch

@pytest.fixture
def mock_auth():
    patcher = patch("myproject.auth")
    mock_auth = patcher.start()
    mock_auth.actual_auth_logic.return_value = ... a simulated "user is logged in" value
    yield
    patcher.stop()

Тогда любой тест представления, который хочет эффективно пропустить аутентификацию (т. е. предположить, что пользователь вошел в систему), может просто использовать это приспособление:

def test_view(client, mock_auth):
    response = client.get('/some/request/path/to/my/view')

    assert response.content == "what I expect in the response content when user is logged in"

Когда я хотел проверить, что пользователь, не прошедший проверку подлинности, не видит аутентифицированный контент, я просто не использовал фиксацию аутентификации:

def test_view_when_user_is_unauthenticated(client):
    response = client.get('/some/request/path/to/my/view')

    assert response.content == "content when user is not logged in"

Это немного хрупко, так как теперь тесты для представления привязаны к внутренностям механизмов аутентификации (т.е. если бы этот метод actual_auth_logic был переименован/рефакторинг, это были бы плохие времена), но, по крайней мере, он изолирован только от фикстуры.

person Adam Parkin    schedule 08.07.2020