関数をラップする前にPythonデコレータにパッチを適用できますか?


83

PythonMockライブラリを使用してテストしようとしているデコレータを備えた関数があります。mock.patch実際のデコレータを、関数を呼び出すだけのモックの「バイパス」デコレータに置き換えるために使用したいと思います。

私が理解できないのは、実際のデコレータが関数をラップする前にパッチを適用する方法です。パッチターゲットでいくつかの異なるバリエーションを試し、パッチとインポートステートメントを並べ替えましたが、成功しませんでした。何か案は?

回答:


59

デコレータは関数定義時に適用されます。ほとんどの関数では、これはモジュールがロードされるときです。(他の関数で定義されている関数では、囲んでいる関数が呼び出されるたびにデコレータが適用されます。)

したがって、デコレータにモンキーパッチを適用する場合は、次のことを行う必要があります。

  1. それを含むモジュールをインポートします
  2. モックデコレータ関数を定義する
  3. 設定 module.decorator = mymockdecorator
  4. デコレータを使用するモジュールをインポートするか、独自のモジュールで使用します

デコレータを含むモジュールにそれを使用する関数も含まれている場合、それらは表示されるまでにすでに装飾されており、おそらくSOLです。

私が最初にこれを書いたのでPythonへの変更を反映するように編集します:デコレータが使用functools.wraps()し、Pythonのバージョンが十分に新しい場合、__wrapped__属性を使用して元の関数を掘り下げて再装飾できる可能性がありますが、これは決してありません保証されており、交換するデコレータだけが適用されるデコレータではない場合もあります。


17
以下は私の時間をかなり浪費しました:Pythonはモジュールを一度だけインポートすることに注意してください。一連のテストを実行していて、テストの1つでデコレータをモックしようとしていて、装飾された関数が他の場所にインポートされている場合、デコレータをモックしても効果はありません。
Paragon 2013年

2
組み込みreload関数を使用して、Pythonバイナリコードdocs.python.org/2/library/functions.html#reloadを再生成し、デコレータをモンキーパッチします
IxDay 2014

3
@Paragonによって報告された問題に遭遇し、テストディレクトリのデコレータにパッチを適用することで問題を回避しました__init__。これにより、テストファイルの前にパッチが確実にロードされました。分離されたテストフォルダーがあるため、戦略は機能しますが、これはすべてのフォルダーレイアウトで機能するとは限りません。
クレイトン2017年

4
これを数回読んだ後、私はまだ混乱しています。これにはコード例が必要です!
ritratt

@claytond分離されたテストフォルダーがあったので、ソリューションが機能してくれてありがとう!
Srivathsa

56

ここでの回答のいくつかは、単一のテストインスタンスではなく、テストセッション全体のデコレータにパッチを適用することに注意してください。これは望ましくない場合があります。1回のテストでしか持続しないデコレータにパッチを適用する方法は次のとおりです。

不要なデコレータでテストするユニット:

# 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()で他のパッチを開始でき、それらはすべて1か所でクリーンアップされます。

この方法について理解する重要なことは、リロードが物事にどのように影響するかです。モジュールに時間がかかりすぎる場合、またはインポート時に実行されるロジックがある場合は、ユニットの一部としてデコレータを肩をすくめてテストする必要があります。:(うまくいけば、あなたのコードはそれよりもうまく書かれています。そうですか?

パッチがテストセッション全体に適用されているかどうかを気にしない場合それを行う最も簡単な方法は、テストファイルの先頭にあります。

# 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、これは私を大いに助けてくれました。受け入れられた答えは良いですが、これは私にとって意味のある方法で物事を綴り、少し違うものが必要になるかもしれない複数のユースケースを含みました。
マルコムジョーンズ

1
この回答ありがとうございます!これが他の人に役立つ場合に備えて、コンテキストマネージャーとして機能し、リロードを行うパッチの拡張を作成しました:gist.github.com/Geekfish/aa43368ceade131b8ed9c822d2163373
Geekfish

13

私が最初にこの問題に遭遇したとき、私は何時間も頭を悩ませていました。私はこれを処理するはるかに簡単な方法を見つけました。

これは、ターゲットが最初から装飾されていなかったように、デコレータを完全にバイパスします。

これは2つの部分に分けられます。次の記事を読むことをお勧めします。

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

私が遭遇し続けた2つの落とし穴:

1.)関数/モジュールをインポートする前にデコレータをモックします。

デコレータと関数は、モジュールのロード時に定義されます。インポートする前にモックを作成しないと、モックは無視されます。ロード後、奇妙な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. テストターゲットをロードするインポートステートメントを削除します。
  2. 上記のように、テストの起動時にデコレータにパッチを適用します。
  3. パッチを適用した直後にimportlib.import_module()を呼び出して、テストターゲットをロードします。
  4. 通常どおりテストを実行します。

それは魅力のように機能しました。


1

文字列のような別のパラメータを取得することもあれば、取得しないこともあるデコレータをモックしようとしました。例:

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

OR

@myDecorator
def function()

上記の回答の1つに感謝し、モック関数を作成し、このモック関数でデコレータにパッチを適用しました。

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

この例は、decorated関数を実行せず、実際に実行する前にいくつかのことを実行するデコレータに適していることに注意してください。デコレータがデコレートされた関数も実行するため、関数のパラメータを転送する必要がある場合、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その後、テストしているモジュールに置き換えることができます。(これはPython3.6で動作MODULEします。xml、たとえば次のようにられます)

OP

あなたのケースでは、のは、モジュール内のデコレータ機能が存在するとしましょうprettyとで装飾された機能が存在するがpresent、その後、あなたがパッチを適用うpretty.decoratorモック機械や代替使用MODULEしてのpresent。次のようなものが機能するはずです(テストされていません)。

クラス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輸入が交換見つけるMODULEsysosこれらは上alreadされているので、失敗するなど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
この回答には多くの問題があります。1つ目(およびより大きなもの)は、まだ装飾されている場合は元の関数にアクセスできないことです(これがOPの問題です)。さらに、テストの完了後にパッチを削除しないため、テストスイートで実行すると問題が発生する可能性があります。
Michele d'Amico
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.