Python + pytestにて、「pytest.raisesを使って例外をアサーションする」テストコードを作成する機会がありました。
ただ、書き方を誤りうまくアサーションできなかっため、メモを残します。
目次
環境
- Python 3.6.0
- unittest.mock.patchを使用
- pytest 3.0.7
状況
テスト対象のメソッドtarget_method()
は、以下とします。
pytest_raises.py
from with_statement_library import Validator class Target(object): def target_method(self): """テスト対象のメソッド""" validator = Validator() validator.run()
テスト対象クラスでimportしているValicationクラスは、(今回省略していますが)長い行に渡って検証処理があり、エラーがあったら最後に例外を送出しています。
pytest_raises_library.py
class Validator(object): def run(self): """長い行検証処理をしていて、エラーがあったら例外を送出するメソッド""" raise RuntimeError
今回のテストでは、run()メソッドをテストするためのデータを用意するのが難しいと仮定して、Validator.run()
をモックに差し替える方法を取ります。
モックを使ったテストコードとして
from unittest.mock import patch, MagicMock import pytest from with_statement import Target class Test_Target(object): def test_mock_patch(self): mock_run = MagicMock() with patch('with_statement.Validator.run', mock_run): sut = Target() actual = sut.target_method() # モックが呼ばれている回数は1回か assert mock_run.call_count == 1
としたところ、テストをパスしました。
続いて、例外を送出するよう、patchとpytest.raisesを使って
def test_mistake_usage_pytest_raises(self): mock_run = MagicMock(side_effect=AssertionError) # with文にpytest.raisesを追加 with patch('with_statement.Validator.run', mock_run), \ pytest.raises(AssertionError): sut = Target() actual = sut.target_method() assert mock_run.call_count == 1
としたところ、これもテストをパスしました。
念のため、失敗するテストとして
def test_mistake_usage_pytest_raises_but_test_pass(self): mock_run = MagicMock(side_effect=AssertionError) with patch('with_statement.Validator.run', mock_run), \ pytest.raises(AssertionError): sut = Target() actual = sut.target_method() # 呼ばれた回数を2回としてアサーションする # 実際に呼ばれるのは1回なので、テストは失敗するはず assert mock_run.call_count == 2
と書いてみたところ、このテストがパスしてしまいました。
原因
pytestの公式ドキュメント(英語版)に記載がありました*1。
When using pytest.raises as a context manager, it’s worthwhile to note that normal context manager rules apply and that the exception raised must be the final line in the scope of the context manager. Lines of code after that, within the scope of the context manager will not be executed.
例外が発生したメソッド以降の処理は、実行されないようでした。
今回の場合、
with patch('with_statement.Validator.run', mock_run), \ pytest.raises(AssertionError): sut = Target() # ここで例外が発生 actual = sut.target_method() # このassertは処理されないため、テストがパスする assert mock_run.call_count == 2
のため、テストがパスしたと考えられました。
対応
with文をネストして、モック用とpytest.raises用に分けます。
def test_correct_usage_pytest_raises_and_test_fail(self): mock_run = MagicMock(side_effect=AssertionError) # モック用のwith文 with patch('with_statement.Validator.run', mock_run): # pytest.raisesのwith文は別途用意 with pytest.raises(AssertionError): sut = Target() # この中の最後に、例外を送出するメソッドを書く actual = sut.target_method() # 検証は、インデントを一つ上げて書く assert mock_run.call_count == 2
テストを実行したところ、正しく失敗しました。
$ pytest ==== test session starts ==== platform darwin -- Python 3.6.0, pytest-3.0.6, py-1.4.32, pluggy-0.4.0 rootdir: /Users/kamijoshinya/thinkami/try/pytest_sample, inifile: pytest.ini collected 4 items test_pytest_raises.py ...F ==== FAILURES ==== ____ Test_Target.test_correct_usage_pytest_raises_and_test_fail ____ ... > assert mock_run.call_count == 2 E assert 1 == 2 E + where 1 = <MagicMock id='4422285968'>.call_count test_pytest_raises.py:58: AssertionError
ソースコード
GitHubにあげました。e.g._pytest_raises
ディレクトリが今回のものです。
thinkAmi-sandbox/python_mock-sample
*1:対象バージョンが古いためか、日本語版には記載がありません