함수를 래핑하기 전에 Python 데코레이터를 패치 할 수 있습니까?


83

Python Mock 라이브러리 의 도움으로 테스트를 시도중인 데코레이터가있는 함수가 있습니다. mock.patch실제 데코레이터를 함수를 호출하는 모의 '바이 패스'데코레이터로 대체하는 데 사용하고 싶습니다 .

내가 알아낼 수없는 것은 실제 데코레이터가 함수를 래핑하기 전에 패치를 적용하는 방법입니다. 패치 대상에 대해 몇 가지 다른 변형을 시도하고 패치 및 가져 오기 문을 재정렬했지만 성공하지 못했습니다. 어떤 아이디어?

답변:


59

데코레이터는 함수 정의 시간에 적용됩니다. 대부분의 기능에서 이것은 모듈이로드 될 때입니다. (다른 함수에 정의 된 함수는 둘러싸는 함수가 호출 될 때마다 적용되는 데코레이터가 있습니다.)

따라서 데코레이터에 원숭이 패치를 적용하려면 다음을 수행해야합니다.

  1. 포함 된 모듈 가져 오기
  2. 모의 데코레이터 함수 정의
  3. 예를 들어 설정 module.decorator = mymockdecorator
  4. 데코레이터를 사용하는 모듈을 가져 오거나 자체 모듈에서 사용합니다.

데코레이터가 포함 된 모듈에이를 사용하는 함수도 포함되어있는 경우 볼 수있을 때 이미 데코레이터가 장식되어 있으며 아마도 SOL 일 것입니다.

내가 원래 작성한 이후 Python에 대한 변경 사항을 반영하도록 편집하십시오. 데코레이터가 사용 functools.wraps()하고 Python 버전이 충분히 새로운 경우 __wrapped__속성을 사용하여 원래 함수를 파 내고 다시 장식 할 수 있지만 이것은 결코 아닙니다 그리고 교체하려는 데코레이터가 유일한 데코레이터가 아닐 수도 있습니다.


17
다음은 내 시간을 상당히 낭비했습니다. Python은 모듈을 한 번만 가져옵니다. 테스트 모음을 실행하고 테스트 중 하나에서 데코레이터를 모의하려고 시도하고 데코 레이팅 된 함수를 다른 곳으로 가져 오면 데코레이터를 모의해도 효과가 없습니다.
Paragon

2
내장 reload함수를 사용하여 파이썬 바이너리 코드 docs.python.org/2/library/functions.html#reload 를 재생성하고 데코레이터를 monkeypatch
IxDay

3
@Paragon이보고 한 문제를 발견하고 테스트 디렉토리의 __init__. 이를 통해 테스트 파일보다 먼저 패치가로드되었습니다. 격리 된 테스트 폴더가 있으므로 전략이 적합하지만 모든 폴더 레이아웃에서 작동하지 않을 수 있습니다.
claytond 2017

4
이것을 여러 번 읽은 후에도 여전히 혼란 스럽습니다. 코드 예제가 필요합니다!
ritratt

@claytond 격리 된 테스트 폴더가 있었기 때문에 솔루션이 저에게 효과적이었습니다!
Srivathsa

56

여기에있는 몇 가지 답변은 단일 테스트 인스턴스가 아닌 전체 테스트 세션에 대한 데코레이터를 패치한다는 점에 유의해야합니다. 바람직하지 않을 수 있습니다. 단일 테스트를 통해서만 지속되는 데코레이터를 패치하는 방법은 다음과 같습니다.

원하지 않는 데코레이터로 테스트 할 유닛 :

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

데코레이터 모듈에서 :

# app/decorators.py

def func_decor(func):
    def inner(*args, **kwargs):
        print "Do stuff we don't want in our test"
        return func(*args, **kwargs)
    return inner

테스트 실행 중에 테스트가 수집 될 때까지 원하지 않는 데코레이터가 이미 테스트중인 단위에 적용되었습니다 (임포트시 발생하기 때문). 이를 제거하려면 데코레이터 모듈의 데코레이터를 수동으로 교체 한 다음 UUT가 포함 된 모듈을 다시 가져와야합니다.

테스트 모듈 :

#  test_uut.py

from unittest import TestCase
from app import uut  # Module with our thing to test
from app import decorators  # Module with the decorator we need to replace
import imp  # Library to help us reload our UUT module
from mock import patch


class TestUUT(TestCase):
    def setUp(self):
        # Do cleanup first so it is ready if an exception is raised
        def kill_patches():  # Create a cleanup callback that undoes our patches
            patch.stopall()  # Stops all patches started with start()
            imp.reload(uut)  # Reload our UUT module which restores the original decorator
        self.addCleanup(kill_patches)  # We want to make sure this is run so we do this in addCleanup instead of tearDown

        # Now patch the decorator where the decorator is being imported from
        patch('app.decorators.func_decor', lambda x: x).start()  # The lambda makes our decorator into a pass-thru. Also, don't forget to call start()          
        # HINT: if you're patching a decor with params use something like:
        # lambda *x, **y: lambda f: f
        imp.reload(uut)  # Reloads the uut.py module which applies our patched decorator

정리 콜백 인 kill_patches는 원래 데코레이터를 복원하고 테스트중인 유닛에 다시 적용합니다. 이렇게하면 패치가 전체 세션이 아닌 단일 테스트를 통해서만 지속됩니다. 이는 다른 패치가 정확히 작동하는 방식입니다. 또한 정리가 patch.stopall ()을 호출하기 때문에 필요한 setUp ()에서 다른 패치를 시작할 수 있으며 한곳에서 모두 정리됩니다.

이 방법에 대해 이해해야 할 중요한 것은 다시로드가 사물에 미치는 영향입니다. 모듈이 너무 오래 걸리거나 가져 오기시 실행되는 로직이있는 경우 단위의 일부로 데코레이터를 어깨를 으쓱하고 테스트해야 할 수도 있습니다. :( 당신의 코드가 그것보다 더 잘 작성 되었으면합니다. 맞죠?

패치가 전체 테스트 세션에 적용되는지 신경 쓰지 않는다면 가장 쉬운 방법은 테스트 파일의 맨 위에있는 것입니다.

# test_uut.py

from mock import patch
patch('app.decorators.func_decor', lambda x: x).start()  # MUST BE BEFORE THE UUT GETS IMPORTED ANYWHERE!

from app import uut

UUT의 로컬 범위가 아닌 데코레이터로 파일을 패치하고 데코레이터로 유닛을 가져 오기 전에 패치를 시작해야합니다.

흥미롭게도 패치가 중지 되더라도 이미 가져온 모든 파일에는 데코레이터에 패치가 적용되어 있습니다. 이는 우리가 시작한 상황과 반대입니다. 이 방법은 패치 자체를 선언하지 않더라도 나중에 가져 오는 테스트 실행의 다른 모든 파일을 패치합니다.


1
user2859458, 이것은 나를 크게 도왔습니다. 받아 들여지는 대답은 좋지만 의미있는 방식으로 내용을 설명했으며 약간 다른 것을 원할 수있는 여러 사용 사례를 포함했습니다.
Malcolm Jones

1
이 응답에 감사드립니다! 이것이 다른 사람들에게 유용 할 경우, 나는 여전히 컨텍스트 관리자로 작동하고 당신을 위해 리로딩을 할 패치의 확장을 만들었습니다 : gist.github.com/Geekfish/aa43368ceade131b8ed9c822d2163373
Geekfish

13

이 문제를 처음 만났을 때 나는 몇 시간 동안 뇌를 찌르는 데 사용합니다. 나는 이것을 처리하는 훨씬 더 쉬운 방법을 찾았습니다.

이것은 대상이 처음에 장식되지 않은 것처럼 장식자를 완전히 우회합니다.

이것은 두 부분으로 나뉩니다. 다음 기사를 읽는 것이 좋습니다.

http://alexmarandon.com/articles/python_mock_gotchas/

내가 계속 마주 친 두 가지 고차 :

1.) 함수 / 모듈을 가져 오기 전에 Decorator를 모의하십시오.

데코레이터와 함수는 모듈이로드 될 때 정의됩니다. 가져 오기 전에 모의하지 않으면 모의를 무시합니다. 로드 후에는 이상한 mock.patch.object를 수행해야하는데, 이는 더욱 실망스러워집니다.

2.) 데코레이터에 대한 올바른 경로를 조롱하고 있는지 확인하십시오.

조롱하는 데코레이터의 패치는 테스트가 데코레이터를로드하는 방법이 아니라 모듈이 데코레이터를로드하는 방법을 기반으로한다는 것을 기억하십시오. 이것이 내가 항상 전체 경로를 사용하여 가져 오기를 제안하는 이유입니다. 이렇게하면 테스트가 훨씬 쉬워집니다.

단계 :

1.) 모의 기능 :

from functools import wraps

def mock_decorator(*args, **kwargs):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            return f(*args, **kwargs)
        return decorated_function
    return decorator

2.) 데코레이터 조롱 :

2a.) 내부 경로.

with mock.patch('path.to.my.decorator', mock_decorator):
     from mymodule import myfunction

2b.) 파일 상단 또는 TestCase.setUp의 패치

mock.patch('path.to.my.decorator', mock_decorator).start()

이러한 방법 중 하나를 사용하면 TestCase 또는 해당 메서드 / 테스트 케이스 내에서 언제든지 함수를 가져올 수 있습니다.

from mymodule import myfunction

2.) mock.patch의 부작용으로 별도의 함수를 사용하십시오.

이제 모의하려는 각 데코레이터에 대해 mock_decorator를 사용할 수 있습니다. 각 데코레이터를 개별적으로 조롱해야하므로 놓친 데코레이터를 조심하세요.


1
당신이 인용 한 블로그 게시물은 내가 이것을 훨씬 더 잘 이해하는 데 도움이되었습니다!
ritratt

2

다음은 나를 위해 일했습니다.

  1. 테스트 대상을로드하는 import 문을 제거하십시오.
  2. 위에 적용된대로 테스트 시작시 데코레이터를 패치합니다.
  3. 패치 후 즉시 importlib.import_module ()을 호출하여 테스트 대상을로드합니다.
  4. 정상적으로 테스트를 실행합니다.

그것은 매력처럼 작동했습니다.


1

우리는 때때로 문자열과 같은 또 다른 매개 변수를받는 데코레이터를 모의하려고했습니다.

@myDecorator('my-str')
def function()

OR

@myDecorator
def function()

위의 답변 중 하나 덕분에 모의 함수를 작성하고이 모의 함수로 데코레이터를 패치했습니다.

from mock import patch

def mock_decorator(f):

    def decorated_function(g):
        return g

    if callable(f): # if no other parameter, just return the decorated function
        return decorated_function(f)
    return decorated_function # if there is a parametr (eg. string), ignore it and return the decorated function

patch('path.to.myDecorator', mock_decorator).start()

from mymodule import myfunction

이 예제는 데코 레이팅 된 함수를 실행하지 않는 데코레이터에 유용하며 실제 실행 전에 일부 작업 만 수행합니다. 데코레이터가 데코 레이팅 된 함수도 실행하므로 함수의 매개 변수를 전송해야하는 경우 mock_decorator 함수는 약간 달라야합니다.

이것이 다른 사람들에게 도움이되기를 바랍니다 ...


0

기본적으로 테스트 모드가 사용되는지 확인하기 위해 일부 구성 변수를 확인하는 모든 데코레이터의 정의에 다른 데코레이터를 적용 할 수 있습니다.
그렇다면, 꾸미고있는 데코레이터를 아무것도하지 않는 더미 데코레이터로 대체합니다.
그렇지 않으면이 데코레이터가 통과 할 수 있습니다.


0

개념

이것은 약간 이상하게 들릴 수 있지만 sys.path자신의 복사본으로 패치 하고 테스트 함수 범위 내에서 가져 오기를 수행 할 수 있습니다. 다음 코드는 개념을 보여줍니다.

from unittest.mock import patch
import sys

@patch('sys.modules', sys.modules.copy())
def testImport():
 oldkeys = set(sys.modules.keys())
 import MODULE
 newkeys = set(sys.modules.keys())
 print((newkeys)-(oldkeys))

oldkeys = set(sys.modules.keys())
testImport()                       -> ("MODULE") # Set contains MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys))         -> set()      # An empty set

MODULE그런 다음 테스트중인 모듈로 대체 될 수 있습니다. (이것은 Python 3.6에서 작동 MODULE하며xml 예를 들어 )

OP

귀하의 경우, 데코레이터 함수가 모듈에 pretty있고 데코 레이팅 된 함수가에 있다고 가정 하면 모의 기계를 사용하여 present패치 pretty.decorator하고 다음으로 대체 MODULE합니다.present . 다음과 같은 것이 작동합니다 (테스트되지 않음).

class TestDecorator (unittest.TestCase) : ...

  @patch(`pretty.decorator`, decorator)
  @patch(`sys.path`, sys.path.copy())
  def testFunction(self, decorator) :
   import present
   ...

설명

이는 테스트 모듈 sys.path의 현재 사본을 사용하여 각 테스트 기능에 대해 "깨끗한" 기능 을 제공함으로써 작동 sys.path합니다. 이 복사본은 모듈이 처음으로 구문 분석 될 때 만들어 지므로sys.path 모든 테스트에 대해 을 합니다.

뉘앙스

그러나 몇 가지 의미가 있습니다. 테스트 프레임 워크가 동일한 Python 세션에서 여러 테스트 모듈을 실행하는 경우 MODULE전역 적으로 가져 오는 모든 테스트 모듈은 로컬로 가져 오는 모든 테스트 모듈을 중단합니다. 이렇게하면 모든 곳에서 로컬로 가져 오기를 수행해야합니다. 프레임 워크가 별도의 Python 세션에서 각 테스트 모듈을 실행하면 작동합니다. 마찬가지로 가져 오는 MODULE테스트 모듈 내에서 전역으로 가져올 수 없습니다.MODULE 로컬로 .

의 하위 클래스 내에서 각 테스트 함수에 대해 로컬 가져 오기를 수행해야합니다 unittest.TestCase. 이것을 적용 할 수 있습니다.unittest.TestCase 서브 클래스에 직접 하여 클래스 내의 모든 테스트 함수에 대해 모듈의 특정 가져 오기를 사용할 수 있도록 할 수 있습니다.

내장 기능

있는 사람의 간섭이 builtin수입 대체 찾을 MODULE함께 sys, os이들이에을 벌써되기 때문에, 실패 등 sys.path당신이 그것을 복사하려고 할 때. 여기서 트릭은 내장 가져 오기를 비활성화 한 상태에서 Python을 호출하는 것 python -X test.py입니다. 그렇게 할 것이라고 생각 하지만 적절한 플래그를 잊어 버렸습니다 (참조 python --help). 이후 import builtins에 IIRC를 사용하여 로컬로 가져올 수 있습니다 .


0

데코레이터를 패치하려면 해당 데코레이터 패치 한 후 해당 데코레이터를 사용하는 모듈을 가져 오거나 다시로드하거나 해당 데코레이터에 대한 모듈의 참조를 모두 다시 정의해야합니다.

데코레이터는 모듈을 가져올 때 적용됩니다. 이것이 파일 상단에 패치하려는 데코레이터를 사용하는 모듈을 임포트하고 나중에 다시로드하지 않고 패치를 시도하면 패치가 효과가없는 이유입니다.

다음은이를 수행하는 첫 번째 방법의 예입니다. 사용하는 데코레이터를 패치 한 후 모듈을 다시로드합니다.

import moduleA
...

  # 1. patch the decorator
  @patch('decoratorWhichIsUsedInModuleA', examplePatchValue)
  def setUp(self)
    # 2. reload the module which uses the decorator
    reload(moduleA)

  def testFunctionA(self):
    # 3. tests...
    assert(moduleA.functionA()...

유용한 참고 자료 :


-2

@lru_cache (max_size = 1000)의 경우


class MockedLruCache(object):

def __init__(self, maxsize=0, timeout=0):
    pass

def __call__(self, func):
    return func

cache.LruCache = MockedLruCache

매개 변수가없는 데코레이터를 사용하는 경우 다음을 수행해야합니다.

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated


1
이 답변에서 많은 문제를 봅니다. 첫 번째 (그리고 더 큰 것)는 아직 장식 된 경우 원래 기능에 액세스 할 수 없다는 것입니다 (즉, OP 문제). 또한 테스트가 완료된 후 패치를 제거하지 않으면 테스트 스위트에서 실행할 때 문제가 발생할 수 있습니다.
Michele d' Amico 2015
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.