を使用しているためpytest
、失敗したテストと対話するための十分なオプションが提供されます。コマンドラインオプションと、これを可能にするいくつかのフックを提供します。それぞれの使用方法と、特定のデバッグのニーズに合わせてカスタマイズできる場所について説明します。
また、本当に必要な場合は、特定のアサーションを完全にスキップできる、よりエキゾチックなオプションについても説明します。
アサートではなく例外を処理する
失敗したテストは通常pytestを停止しないことに注意してください。明示的に有効にして、特定の数の失敗後に終了するように明示的に指示した場合のみ。また、例外が発生するため、テストは失敗します。assert
がAssertionError
発生しますが、テストが失敗する唯一の例外ではありません!変更ではなく、例外の処理方法を制御したいassert
。
ただし、アサートに失敗すると、個々のテストが終了します。これは、try...except
ブロックの外で例外が発生すると、Pythonが現在の関数フレームを巻き戻し、それ以上戻ることがないためです。
_assertCustom()
アサーションを再実行する試みの説明から判断すると、それはあなたが望んでいることではないと思いますが、それでも、オプションについては後で詳しく説明します。
pdbを使用したpytestでの事後デバッグ
デバッガーでの失敗を処理するためのさまざまなオプションについては、テストが失敗したときに標準のデバッグプロンプトを開く--pdb
コマンドラインスイッチから始めます(簡潔にするために出力を省略しています)。
$ mkdir demo
$ touch demo/__init__.py
$ cat << EOF > demo/test_foo.py
> def test_ham():
> assert 42 == 17
> def test_spam():
> int("Vikings")
> EOF
$ pytest demo/test_foo.py --pdb
[ ... ]
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(2)test_ham()
-> assert 42 == 17
(Pdb) q
Exit: Quitting debugger
[ ... ]
このスイッチを使用すると、テストが失敗するとpytestが事後デバッグセッションを開始します。これは基本的にはまさにあなたが望んでいたものです。テストが失敗した時点でコードを停止し、デバッガーを開いてテストの状態を確認します。テストのローカル変数、グローバル、およびスタック内のすべてのフレームのローカルとグローバルを操作できます。
ここでpytestは、この時点で終了するかどうかを完全に制御しますq
。quitコマンドを使用すると、pytestは実行も終了します。forcontinue を使用するc
と、制御がpytestに戻り、次のテストが実行されます。
代替デバッガーの使用
このため、pdb
デバッガーに拘束されません。--pdbcls
スイッチで別のデバッガーを設定できます。IPythonデバッガーの実装、または他のほとんどのPythonデバッガー(pudbデバッガーでは、スイッチを使用する必要があるか、特別なプラグイン)を含め、pdb.Pdb()
互換性のある任意の実装が機能します。スイッチはモジュールとクラスを受け取ります。たとえば、次のように使用できます。-s
pudb
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
あなたは周り独自のラッパークラスを書くためにこの機能を使用することができますPdb
ことは、単にすぐに戻り、特定の障害はあなたが興味のあるものではありません場合。pytest
用途をPdb()
正確に似pdb.post_mortem()
てい:
p = Pdb()
p.reset()
p.interaction(None, t)
ここで、t
あるトレースバックオブジェクトが。p.interaction(None, t)
戻ったとき、pytest
が(その時点でpytestが終了する)に設定されていない限り 、次のテストを続行します。p.quitting
True
以下は、テストが発生しない限り、デバッグを拒否してすぐに戻ることを出力しValueError
、次のように保存した実装例demo/custom_pdb.py
です。
import pdb, sys
class CustomPdb(pdb.Pdb):
def interaction(self, frame, traceback):
if sys.last_type is not None and not issubclass(sys.last_type, ValueError):
print("Sorry, not interested in this failure")
return
return super().interaction(frame, traceback)
これを上記のデモで使用すると、出力されます(ここでも簡潔にするために省略しています)。
$ pytest test_foo.py -s --pdb --pdbcls=demo.custom_pdb:CustomPdb
[ ... ]
def test_ham():
> assert 42 == 17
E assert 42 == 17
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sorry, not interested in this failure
F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
上記のイントロスペクトsys.last_type
は、障害が「興味深い」かどうかを判断します。
ただし、tkInterまたは同様の何かを使用して独自のデバッガーを作成する場合を除いて、このオプションを実際にお勧めすることはできません。これは大きな仕事であることに注意してください。
フィルタリングの失敗; デバッガーを開くタイミングを選択します
次のレベルはpytestのデバッグと相互作用のフックです。これらは、動作のカスタマイズのフックポイントであり、pytestが例外の処理や、pdb.set_trace()
またはbreakpoint()
(Python 3.7以降)を介してデバッガーに入るなどの通常の処理方法を置換または強化します。
このフックの内部実装は>>> entering PDB >>>
上記のバナーの印刷も担当するため、このフックを使用してデバッガーが実行されないようにすると、この出力はまったく表示されなくなります。独自のフックを作成して、テストの失敗が「興味深い」ときに元のフックに委任することができるため、使用しているデバッガーとは無関係にテストの失敗をフィルター処理できます。名前でアクセスすることにより、内部実装にアクセスできます。このための内部フックプラグインはという名前pdbinvoke
です。実行されないようにするには、登録を解除する必要がありますが、参照を保存する必要があります。必要に応じて直接呼び出すことができます。
このようなフックの実装例を以下に示します。これは、プラグインが読み込まれる任意の場所に配置できます。私はそれを入れましたdemo/conftest.py
:
import pytest
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
# unregister returns the unregistered plugin
pdbinvoke = config.pluginmanager.unregister(name="pdbinvoke")
if pdbinvoke is None:
# no --pdb switch used, no debugging requested
return
# get the terminalreporter too, to write to the console
tr = config.pluginmanager.getplugin("terminalreporter")
# create or own plugin
plugin = ExceptionFilter(pdbinvoke, tr)
# register our plugin, pytest will then start calling our plugin hooks
config.pluginmanager.register(plugin, "exception_filter")
class ExceptionFilter:
def __init__(self, pdbinvoke, terminalreporter):
# provide the same functionality as pdbinvoke
self.pytest_internalerror = pdbinvoke.pytest_internalerror
self.orig_exception_interact = pdbinvoke.pytest_exception_interact
self.tr = terminalreporter
def pytest_exception_interact(self, node, call, report):
if not call.excinfo. errisinstance(ValueError):
self.tr.write_line("Sorry, not interested!")
return
return self.orig_exception_interact(node, call, report)
上記のプラグインは、内部TerminalReporter
プラグインを使用して端末に行を書き込みます。これにより、デフォルトのコンパクトテストステータス形式を使用するときに出力がよりクリーンになり、出力キャプチャが有効になっていても、端末に物を書き込むことができます。
この例でpytest_exception_interact
はpytest_configure()
、別のフックを介してプラグインオブジェクトをフックに登録していますが@pytest.hookimpl(trylast=True)
、内部pdbinvoke
プラグインの登録を解除できるように(を使用して)十分に遅く実行されていることを確認してください。フックが呼び出されると、サンプルはcall.exceptinfo
オブジェクトに対してテストします。ノードまたはレポートも確認できます。
上記のサンプルコードをに配置するdemo/conftest.py
と、test_ham
テストエラーは無視され、test_spam
テストエラーのみが発生ValueError
し、デバッグプロンプトが開きます。
$ pytest demo/test_foo.py --pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
繰り返しますが、上記のアプローチには、これをputestを含むpytestで動作するデバッガーまたはIPythonデバッガーと組み合わせることができるという追加の利点があります。
$ pytest demo/test_foo.py --pdb --pdbcls=IPython.core.debugger:Pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
1 def test_ham():
2 assert 42 == 17
3 def test_spam():
----> 4 int("Vikings")
ipdb>
また、(node
引数を介して)どのテストが実行されたかについてのコンテキストが多くなり、(call.excinfo
ExceptionInfo
インスタンスを介して)発生した例外に直接アクセスできます。
特定のpytestデバッガプラグイン(pytest-pudb
またはなどpytest-pycharm
)は独自のpytest_exception_interact
フックスプを登録することに注意してください。より完全な実装は、任意のプラグインを自動的にオーバーライドして使用しconfig.pluginmanager.list_name_plugin
、hasattr()
各プラグインをテストするために、plugin-manager内のすべてのプラグインをループする必要があります。
失敗を完全になくす
これにより、失敗したテストのデバッグを完全に制御できますが、特定のテストでデバッガーを開かないことを選択した場合でも、テストは失敗したままになります。失敗を完全に解消したい場合は、別のフックを使用できますpytest_runtest_call()
。
pytestがテストを実行する場合、上記のフックを介してテストを実行します。フックNone
は例外を返すか、発生させることが期待されています。これからレポートが作成され、オプションでログエントリが作成さpytest_exception_interact()
れます。テストが失敗した場合は、前述のフックが呼び出されます。したがって、このフックが生成する結果を変更するだけで済みます。例外の代わりに、何も返すべきではありません。
これを行う最良の方法は、フックラッパーを使用することです。フックラッパーは実際の作業を行う必要はありませんが、代わりにフックの結果に何が起こるかを変更する機会が与えられます。次の行を追加するだけです。
outcome = yield
あなたのフックラッパー実装で、あなたは経由でテスト例外を含むフック結果にアクセスできますoutcome.excinfo
。テストで例外が発生した場合、この属性は(タイプ、インスタンス、トレースバック)のタプルに設定されます。または、outcome.get_result()
標準のtry...except
処理を呼び出して使用することもできます。
では、どのようにして失敗したテストに合格するのですか?3つの基本オプションがあります。
何を使うかはあなた次第です。テストが失敗したかのようにそれらのケースを処理する必要がないため、スキップされたテストと予期された失敗のテストの結果を必ず確認してください。あなたはこれらのオプションを経由して上げる特殊な例外にアクセスすることができますpytest.skip.Exception
し、pytest.xfail.Exception
。
以下は、raiseを行わない失敗したテストをスキップ済みValueError
としてマークする実装例です。
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
outcome = yield
try:
outcome.get_result()
except (pytest.xfail.Exception, pytest.skip.Exception, pytest.exit.Exception):
raise # already xfailed, skipped or explicit exit
except ValueError:
raise # not ignoring
except (pytest.fail.Exception, Exception):
# turn everything else into a skip
pytest.skip("[NOTRUN] ignoring everything but ValueError")
conftest.py
出力に入れると次のようになります:
$ pytest -r a demo/test_foo.py
============================= test session starts =============================
platform darwin -- Python 3.8.0, pytest-3.10.0, py-1.7.0, pluggy-0.8.0
rootdir: ..., inifile:
collected 2 items
demo/test_foo.py sF [100%]
=================================== FAILURES ===================================
__________________________________ test_spam ___________________________________
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
=========================== short test summary info ============================
FAIL demo/test_foo.py::test_spam
SKIP [1] .../demo/conftest.py:12: [NOTRUN] ignoring everything but ValueError
===================== 1 failed, 1 skipped in 0.07 seconds ======================
-r a
フラグを使用して、test_ham
今はスキップされたことを明確にしました。
pytest.skip()
呼び出しをpytest.xfail("[XFAIL] ignoring everything but ValueError")
に置き換えると、テストは予期される失敗としてマークされます。
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
使用するとoutcome.force_result([])
マークされます:
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
ユースケースに最も適していると思うのはあなた次第です。以下のためにskip()
とxfail()
、私は、標準のメッセージ形式を模倣(接頭辞[NOTRUN]
または[XFAIL]
)しかし、あなたが望む任意の他のメッセージ・フォーマットを自由に使用できます。
3つのケースすべてで、pytestは、このメソッドを使用して結果を変更したテストのデバッガーを開きません。
個々のアサートステートメントの変更
assert
テスト内のテストを変更する場合は、さらに多くの作業を行うように設定します。はい、これは技術的には可能ですが、Pythonがコンパイル時に実行するコードそのものを書き直すことによってのみ可能です。
を使用する場合pytest
、これは実際にはすでに行われています。Pytest はassert
ステートメントを書き換えて、アサートが失敗したときにコンテキストを提供します。何が行われているのかについての概要とソースコードについては、このブログ投稿を参照してください。このモジュールは1k行を超えるため、Pythonの抽象構文ツリーがどのように機能するかを理解する必要があります。そうした場合、あなたは可能性があり、周囲を含め、そこに独自の変更を加えるためにそのモジュールをモンキーパッチとハンドラ。_pytest/assertion/rewrite.py
assert
try...except AssertionError:
ただし、アサートを選択的に無効化または無視することはできません。後続のステートメントは、スキップされたアサートが保護することを意図した状態(特定のオブジェクト配置、変数セットなど)に簡単に依存する可能性があるためです。アサートテスト場合はfoo
されていないNone
、その後アサートはに依存してfoo.bar
存在するように、あなたは単にに実行されますAttributeError
あなたはこのルートを行くために必要がある場合は、再調達例外になど、そこドゥスティックを。
asserts
ここで書き直すことについてこれ以上詳しく説明するつもりはありません。これは、関係する作業量を考慮せずに、これを実行する価値はないと思います。また、事後デバッグにより、テストの状態にアクセスできます。とにかくアサーションエラーのポイント。
これを実行したい場合は、使用する必要はありませんeval()
(いずれにしても機能しないassert
ため、ステートメントなのでexec()
、代わりに使用する必要があります)。また、アサーションを2回実行する必要もないことに注意してください。アサーションで使用されている式が状態を変更した場合、問題が発生する可能性があります)。代わりに、ast.Assert
ノードをノード内に埋め込み、ast.Try
空のast.Raise
ノードを使用する例外ハンドラをアタッチして、キャッチされた例外を再発生させます。
デバッガーを使用してアサーションステートメントをスキップします。
Pythonデバッガでは、実際に/ コマンドを使用してステートメントをスキップできます。特定のアサーションが失敗することが前もってわかっている場合は、これを使用してバイパスできます。すべてのテストの開始時にデバッガーを開くを使用してテストを実行し、デバッガーがアサートの直前で一時停止しているときにa を発行してスキップすることができます。j
jump
--trace
j <line after assert>
これを自動化することもできます。上記のテクニックを使用して、カスタムデバッガプラグインを構築できます。
pytest_testrun_call()
フックを使用してAssertionError
例外をキャッチします
- トレースバックから「問題のある」行番号を抽出し、おそらくいくつかのソースコード分析により、ジャンプの実行に必要なアサーションの前後の行番号を決定します
- はテストを再度実行しますが、今回
Pdb
はアサートの前の行にブレークポイントを設定し、ブレークポイントに到達すると2番目にジャンプしてからc
継続するサブクラスを使用します。
または、アサーションが失敗するのを待つ代わりassert
に、テストで見つかったそれぞれのブレークポイントの設定を自動化できます(ソースコード分析を使用して、テストast.Assert
のASTのノードの行番号を簡単に抽出できます)。アサートされたテストを実行します。デバッガスクリプトコマンドを使用し、jump
コマンドを使用してアサーション自体をスキップします。あなたはトレードオフをする必要があるでしょう。デバッガーの下ですべてのテストを実行します(インタープリターはすべてのステートメントに対してトレース関数を呼び出す必要があるため、処理が遅くなります)。または、これを失敗したテストにのみ適用し、それらのテストを最初から再実行する代償を払ってください。
このようなプラグインは作成するのに多くの作業が必要になるため、ここでは例を記述しません。これは、とにかくそれが回答に収まらないため、また、時間の価値がないと思うためです。デバッガーを開いて手動でジャンプします。アサートの失敗は、テスト自体またはテスト中のコードのいずれかにバグがあることを示しているため、問題のデバッグに集中することもできます。