PythonMockライブラリを使用してテストしようとしているデコレータを備えた関数があります。mock.patch
実際のデコレータを、関数を呼び出すだけのモックの「バイパス」デコレータに置き換えるために使用したいと思います。
私が理解できないのは、実際のデコレータが関数をラップする前にパッチを適用する方法です。パッチターゲットでいくつかの異なるバリエーションを試し、パッチとインポートステートメントを並べ替えましたが、成功しませんでした。何か案は?
PythonMockライブラリを使用してテストしようとしているデコレータを備えた関数があります。mock.patch
実際のデコレータを、関数を呼び出すだけのモックの「バイパス」デコレータに置き換えるために使用したいと思います。
私が理解できないのは、実際のデコレータが関数をラップする前にパッチを適用する方法です。パッチターゲットでいくつかの異なるバリエーションを試し、パッチとインポートステートメントを並べ替えましたが、成功しませんでした。何か案は?
回答:
デコレータは関数定義時に適用されます。ほとんどの関数では、これはモジュールがロードされるときです。(他の関数で定義されている関数では、囲んでいる関数が呼び出されるたびにデコレータが適用されます。)
したがって、デコレータにモンキーパッチを適用する場合は、次のことを行う必要があります。
module.decorator = mymockdecorator
デコレータを含むモジュールにそれを使用する関数も含まれている場合、それらは表示されるまでにすでに装飾されており、おそらくSOLです。
私が最初にこれを書いたのでPythonへの変更を反映するように編集します:デコレータが使用functools.wraps()
し、Pythonのバージョンが十分に新しい場合、__wrapped__
属性を使用して元の関数を掘り下げて再装飾できる可能性がありますが、これは決してありません保証されており、交換するデコレータだけが適用されるデコレータではない場合もあります。
reload
関数を使用して、Pythonバイナリコードdocs.python.org/2/library/functions.html#reloadを再生成し、デコレータをモンキーパッチします
__init__
。これにより、テストファイルの前にパッチが確実にロードされました。分離されたテストフォルダーがあるため、戦略は機能しますが、これはすべてのフォルダーレイアウトで機能するとは限りません。
ここでの回答のいくつかは、単一のテストインスタンスではなく、テストセッション全体のデコレータにパッチを適用することに注意してください。これは望ましくない場合があります。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のローカルスコープではなくデコレータを使用してファイルにパッチを適用し、パッチを開始してください。
興味深いことに、パッチが停止された場合でも、すでにインポートされたすべてのファイルには、デコレータにパッチが適用されたままになります。これは、最初の状況とは逆です。このメソッドは、パッチを自分で宣言していなくても、後でインポートされるテスト実行内の他のファイルにパッチを適用することに注意してください。
私が最初にこの問題に遭遇したとき、私は何時間も頭を悩ませていました。私はこれを処理するはるかに簡単な方法を見つけました。
これは、ターゲットが最初から装飾されていなかったように、デコレータを完全にバイパスします。
これは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を使用できます。各デコレータを個別にモックする必要があるため、見逃したデコレータに注意してください。
文字列のような別のパラメータを取得することもあれば、取得しないこともあるデコレータをモックしようとしました。例:
@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関数は少し異なる必要があります。
これが他の人に役立つことを願っています...
たぶん、すべてのデコレータの定義に別のデコレータを適用して、基本的にいくつかの構成変数をチェックして、テストモードが使用されることを意図しているかどうかを確認できます。
はいの場合、それはそれが装飾しているデコレータを何もしないダミーのデコレータに置き換えます。
それ以外の場合は、このデコレータを通過させます。
これは少し奇妙に聞こえるかもしれませんが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
、たとえば次のようにられます)
あなたのケースでは、のは、モジュール内のデコレータ機能が存在するとしましょう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
輸入が交換見つけるMODULE
とsys
、os
これらは上alreadされているので、失敗するなどsys.path
、あなたがそれをコピーしようとします。ここでの秘訣は、組み込みのインポートを無効にしてPythonを呼び出すpython -X test.py
ことです。これでうまくいくと思いますが、適切なフラグを忘れています(を参照python --help
)。これらはその後import builtins
、IIRCを使用してローカルにインポートできます。
デコレータにパッチを適用するには、パッチを適用した後にそのデコレータを使用するモジュールをインポートまたはリロードするか、モジュールのデコレータへの参照を完全に再定義する必要があります。
デコレータは、モジュールのインポート時に適用されます。これが、ファイルの先頭にパッチを適用するデコレータを使用するモジュールをインポートし、後でリロードせずにパッチを適用しようとした場合、パッチが効果を発揮しない理由です。
これを行う最初の方法の例を次に示します。使用するデコレータにパッチを適用した後、モジュールをリロードします。
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()...
役立つ参考資料:
@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