web-dev-qa-db-ja.com

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

Python Mock ライブラリの助けを借りてテストしようとしているデコレータを持つ関数があります。mock.patchを使用して、関数を呼び出すだけのモック「バイパス」デコレータを備えた実際のデコレータ。実際のデコレータが関数をラップする前にパッチを適用する方法はわかりません。パッチとインポートのステートメントですが、成功していません。

62
Chris Sears

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

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

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

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

Pythonを最初に書いてからの変更を反映するように編集します。デコレータがfunctools.wraps()を使用し、Pythonのバージョンが十分に新しい場合、 ___wrapped___ attritubeを使用して元の関数を掘り出し、それを再装飾できる場合がありますが、これは決して保証されるものではなく、置き換えるデコレーターだけが適用されるデコレーターではない場合があります。

49
kindall

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

望ましくないデコレータでテストするユニット:

# 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のローカルスコープではなく、デコレータを使用してファイルにパッチを適用し、デコレータを使用してユニットをインポートする前にパッチを開始してください。

興味深いことに、パッチを停止しても、既にインポートしたすべてのファイルにはデコレーターにパッチが適用されたままになります。これは、最初に行った状況の逆です。このメソッドは、後で実行されるテスト実行の他のファイルにパッチを適用することに注意してください-パッチ自体を宣言していなくても。

38
user2859458

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

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

これは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を使用できます。各デコレータを個別にモックする必要があるため、見逃しているデコレータに注意してください。

4
user7815681

次は私のために働いた:

  1. テストターゲットをロードするインポートステートメントを削除します。
  2. 上記のように、テストの起動時にデコレータにパッチを適用します。
  3. パッチを適用した直後にimportlib.import_module()を呼び出して、テストターゲットをロードします。
  4. テストを正常に実行します。

それは魅力のように働いた。

1
Eric Mintz

たぶん、基本的にいくつかの構成変数をチェックしてテストモードが使用されることを意図しているかどうかを確認する別のデコレータをすべてのデコレータの定義に適用できます。
はいの場合、装飾しているデコレータを、何もしないダミーのデコレータに置き換えます。
それ以外の場合、このデコレータを通過させます。

0
Aditya Mukherji

概念

これは少し奇妙に聞こえるかもしれませんが、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でMODULExmlに置き換えて動作します)

OP

あなたの場合、デコレータ関数がモジュールprettyにあり、装飾された関数がpresentにあるとします。次に、モック機構を使用してpretty.decoratorにパッチを適用し、MODULE with 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 importsをいじるのは、MODULEsysで置き換えることを見つけるでしょう。osなどは、sys.pathで既にあるので失敗しますコピーしてみてください。ここでのトリックは、ビルトインインポートを無効にしてPythonを呼び出すことです。python -X test.pyはそれを行うと思いますが、適切なフラグを忘れます(python --helpを参照)。 import builtins、IIRCを使用してローカルにインポートされます。

0
Carel