pytest入門 †
インストール †pip install pytest pip install pytest-cov ファイル/フォルダ構成 †https://docs.pytest.org/en/latest/goodpractices.html#choosing-a-test-layout-import-rules pytestのドキュメントにはいくつかの構成が記載されているが src と tests とフォルダ分ける方のが良さげ。 setup.py src フォルダのPATHの通し方 †src と tests を分ける場合で src フォルダに PATH を通すには、環境変数 PYTHONPATH を利用するか、テストコード全体の前処理(conftest.py) で sys.path に追加する。
__init__.py の配置についての注意点 †・ src 直下には __init__.py は置かない。 src/ tests 配下のパッケージ/モジュール名のフルPATHが src 配下と全く同じになってしまうと、src 側のパッケージ配下のモジュールがうまく認識されない模様。( ImportError になる ) イメージ)
セットアップコードと conftest.py †テストコードの前処理、後処理を行う仕組みとして fixture が用意されているが、fixture は conftest.py として別ファイルに切り出す事ができる。 tests/ テストコードの基本形 †以下の命名規則に従っていれば、テストコードとして認識される。
例) def test_sample_function1(): print("test_sample_function1") assert True def test_sample_function2(): print("test_sample_function2") assert False class TestSampleClass(): def test_class_function(): print("test_class_function1") assert True assert の書き方 †https://docs.pytest.org/en/latest/assert.html 基本形 †assert 真偽値(※) [, エラーメッセージ] ※ True の時に成功となる。 文字列マッチング †assert 'match string' in str(result) 例外を期待する場合 †with pytest.raises(ZeroDivisionError): 1 / 0 with pytest.raises(ValueError, match=r'.* 123 .*'): myfunc() fixtureの利用(テスト前後に行う処理) †fixtureを使用してテストの前後に行う処理を指定する事ができるので、これを利用してテストデータの投入などの行うのが良い。 scope について †fixture に scope を指定する事で、どの単位の前処理、後処理なのか明示する事ができる。
例) import pytest @pytest.fixture(scope='session', autouse=True) def session_fixture(): print("テスト全体の前処理") yield print("テスト全体の後処理") @pytest.fixture(scope='module', autouse=True) def module_fixture(): print("モジュールの前処理") yield print("モジュールの後処理") @pytest.fixture(scope='class', autouse=True) def class_fixture(): print("クラスの前処理") yield print("クラスの後処理") @pytest.fixture(scope='function', autouse=True) def function_fixture(): print("関数の前処理") yield print("関数の後処理") yield について †fixture 内に yield を記述する事で、テスト後に行う処理を記述する事が出来る。 @pytest.fixture(scope='function', autouse=True) def module_fixture(): print("関数の前処理") yield print("関数の後処理") fixture からパラメータを受け取る †yield に引数を付けて実行すると、テストコード側で yield の引数を受け取る事ができる。 import pytest import datetime @pytest.fixture(scope='module', autouse=True) def module_fixture1(): print("モジュールの前処理") now = datetime.datetime.now() yield {'now': now} print("モジュールの後処理") def test_fixture_param(module_fixture1): # 引数に対象のfixtureの関数名を指定する now = module_fixture1['now'] # fixture側で生成したインスタンスを受け取る print(f'now: {now}') 実行する fixture をテストコード毎に指定する †以下の例では、test_fixture_sample2 の時のみ、前処理、後処理として fixture_for_sample2 が実行される。 import pytest @pytest.fixture def fixture_for_sample2(): print("サンプル2用の前処理") yield print("サンプル2用の後処理") def test_fixture_sample1(): print('test_fixture_sample1') def test_fixture_sample2(fixture_for_sample2): # 引数に実行するfixtureを指定する print('test_fixture_sample2') def test_fixture_sample3(): print('test_fixture_sample3') mark の利用 †同じテストを値を変えて行う †pytest.mark.parametrize を使用すると、同じメソッドのテストを引数を変えてテストする事ができる。 pytest.mark.parametrize を使用しない場合 import pytest # テスト対象の関数 def sample_div(a, b): return a / b def test_sample_div(): assert sample_div(10, 5) == 2 assert sample_div(1, 1) == 1 assert sample_div(10, 1) == 10 with pytest.raises(ZeroDivisionError): sample_div(a, b) pytest.mark.parametrize を使用する場合 import pytest # テスト対象の関数 def sample_div(a, b): return a / b @pytest.mark.parametrize('a, b, expected', [ (10, 5, 2), (1, 1, 1), (10, 1, 10), (10, 0, ZeroDivisionError), ]) def test_sample_div(a, b, expected): if type(expected) == int: assert sample_div(a, b) == expected else: with pytest.raises(expected): sample_div(a, b) テストをグループ化する †pytest.mark に続いて任意の名前を付けておくと、テストコードをグループ化する事ができ、グループ毎にテストを実行する事ができる。 import pytest @pytest.mark.service1 def test_sample_func1(): print('test_sample_func1') @pytest.mark.service1 def test_sample_func2(): print('test_sample_func2') @pytest.mark.service2 def test_sample_func3(): print('test_sample_func3') 実行例 ( service1 のテストコードのみ実行する ) pytest -m service1 -v 使用するfixtureをデコレータで指定する †メソッド引数にfixture名を指定するのではなく、@pytest.mark.usefixtures を利用する方法もある。 import pytest @pytest.fixture(scope="module") def sample_usefixtures(): print('sample_usefixtures .. 前処理') yield print('sample_usefixtures .. 後処理') @pytest.mark.usefixtures('sample_usefixtures') def test_usefixtures(): print('test_usefixtures!') pytest のドキュメントには unittest.TestCase でテストクラスのインスタンス変数を初期化する方法が記載されており、こっちが主な使い方になるのかも。 クラスやモジュールの動作を動的に変更する †monkeypatchを使用する †テスト対象モジュール def sum_all(*args): # result = sum(args) result = 0 for i in args: result += i return result テストコード import pytest from package1 import module1 # 適当な値を返すモック def dummy_sum_all(*args): return 123 def test_sum_all(monkeypatch): print(monkeypatch) # 内容を差し替え ( monkeypatch.setattr で対象メソッドを差し替える ) #monkeypatch.setattr(module1, 'sum_all', (lambda *x: 123)) # lambda式で書く場合 monkeypatch.setattr(module1, 'sum_all', dummy_sum_all) # テスト対象のコードを実行 result = module1.sum_all(1, 2, 3, 4, 5) # 期待値との比較 expected = 15 assert expected == result, f'期待値と異なります。期待値: {expected}, 結果: {result}' # 123を返却するように差し替えているのでエラーになる ※monkeypatch で使用できるメソッド 例外を発生させたい場合 †unittest.mock を使用する †前述のmonkeypatchを使用しても可能だが、ここでは unittest.mock を使用する例を挙げる。 例えば、以下のコードで hello メソッド内で例外を発生させたい場合、 mymodule.py class MyClass1(object): def __init__(self): self.pre_message = 'Hello' def hello(self, name): return MyStringUtil.concat(self.pre_message, name) class MyStringUtil(object): @classmethod def concat(cls, *args, suffix='!'): return ' '.join(args) + suffix クラスメソッドで例外を発生させたい場合は、@patch デコレータを使用 test_mymodule.py import mymodule from unittest.mock import patch from unittest.mock import Mock # from unittest.mock import MagicMock # 呼び出し回数などを検証したい場合に使用する @patch("mymodule.MyStringUtil.concat", Mock(side_effect=ValueError('Dummy Error!!'))) def test_hello(): with pytest.raises(ValueError): mymodule.MyClass1().hello('Taro') インスタンスメソッドで例外を発生させたい場合は @patch.object を使用 test_mymodule.py import mymodule from unittest.mock import patch from unittest.mock import Mock @patch.object(mymodule.MyClass1, 'hello', Mock(side_effect=ValueError('Dummy Error!!'))) def test_hello(): with pytest.raises(ValueError): mymodule.MyClass1().hello('Taro') with 構文を使用してもOK from unittest.mock import patch from unittest.mock import Mock def test_hello(): with patch("mymodule.MyStringUtil.concat", Mock(side_effect=ValueError('Dummy Error!!'))) as my_mock: with pytest.raises(ValueError): mymodule.MyClass1().hello('Taro') Tips †標準出力に表示する †pytest -s 処理時間を計測する †pytest --durations=0 pytest -v --duration=0 テストケース別に結果を表示する †pytest -v 前回NGだったケースだけテストする †pytest -v --lf 前回NGだったケースからテストする †pytest -v --ff 遅いテストケースを見つける †pytest -v --duration=5 テストのログをファイルに出力する †pytest -v --result-log=tests/log.txt カバレッジを確認する †pytest -v --cov=ディレクトリ HTML形式のカバレッジレポートを出力する †pytest -v --cov=ディレクトリ --cov-report=html テストで実行されなかった行を表示する †pytest -v --cov=ディレクトリ --cov-report=term-missing |