#author("2018-10-08T14:03:40+00:00","","") * pytest入門 [#e9ffd5bd] #setlinebreak(on); #contents #html(<style>.lh17 * { line-height: 1.7}</style>) ** インストール [#q4177cd9] #html(<div style="padding-left:10px;">) #myterm2(){{ pip install pytest pip install pytest-cov }} #html(</div>) ** ファイル/フォルダ構成 [#f0395e6c] #html(<div style="padding-left:10px;">) https://docs.pytest.org/en/latest/goodpractices.html#choosing-a-test-layout-import-rules pytestのドキュメントにはいくつかの構成が記載されているが src と tests とフォルダ分ける方のが良さげ。 #html(<div style="padding:0 40px 0 10px; background:#efefef;border: 1px solid #333;">) setup.py src/ mypkg/ __init__.py app.py view.py tests/ __init__.py foo/ __init__.py test_view.py bar/ __init__.py test_view.py #html(</div>) *** src フォルダのPATHの通し方 [#y2046859] #html(<div style="padding-left:10px;">) src と tests を分ける場合で src フォルダに PATH を通すには、環境変数 PYTHONPATH を利用するか、テストコード全体の前処理(conftest.py) で sys.path に追加する。 ※ テストコード自体の検索PATH は pytest.ini または pytest コマンドオプションの testpaths で指定する。 - 環境変数で行う場合 #myterm2(){{ export PYTHONPATH=./src }} - テストコードの前処理で行う場合 tests/conftest.py #mycode2(){{ import sys import os sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src/")) }} ※ conftest.py については後述。 #html(</div>) *** __init__.py の配置についての注意点 [#xd11be7f] #html(<div style="padding-left:20px;">) ・ src 直下には __init__.py は置かない。 ・ tests 直下には __init__.py を置く。 #html(<div class="lh17" style="padding:0 40px 0 10px; background:#efefef;border: 1px solid #333;display:inline-block;vertical-align:top;">) src/ &color(red){%%__init__.py%%}; ... src直下には置かない mypkg/ myfunc.py tests/ &color(blue){__init__.py}; ... tests直下には置く mypkg test_myfunc.py #html(</div>) tests 配下のパッケージ/モジュール名のフルPATHが src 配下と全く同じになってしまうと、src 側のパッケージ配下のモジュールがうまく認識されない模様。( ImportError になる ) なので、tests 配下に __init__.py をおいて全てのテストコードのパッケージの頭には必ず tests. が付くようにする。 イメージ) |src配下|mypkg.myfunc| |tests配下|tests.mypkg.myfunc| #html(</div>) *** セットアップコードと conftest.py [#q5d6c439] #html(<div style="padding-left:20px;">) テストコードの前処理、後処理を行う仕組みとして fixture が用意されているが、fixture は conftest.py として別ファイルに切り出す事ができる。 また、conftest.py は各フォルダ毎に配置する事ができるので、fixture の scope=session を組み合わせると、各フォルダ単位で行う前後処理を記述する事ができる。 ※フォルダ単位で用意する共通のテストデータの投入などはここで行うと効率的。 #html(<div class="lh17" style="padding:0 40px 0 10px; background:#efefef;border: 1px solid #333;display:inline-block;vertical-align:top;">) tests/ __init__.py &color(red){conftest.py}; ... テスト全体にかかるセットアップコード mypkg1 __init__.py &color(red){conftest.py}; ... mypkg1 配下のテスト用のセットアップコード test_myfunc1.py test_myfunc2.py mypkg2 __init__.py &color(red){conftest.py}; ... mypkg2 配下のテスト用のセットアップコード test_myfunc3.py test_myfunc4.py #html(</div>) #html(</div>) #html(</div>) // ファイル/フォルダ構成 ** テストコードの基本形 [#kcb84353] #html(<div style="padding-left:10px;">) 以下の命名規則に従っていれば、テストコードとして認識される。 - ファイル名のプレフィックスに test_ または サフィックスに _test を付与する。 - テストメソッドのプレフィックスに test_ または サフィックスに _test を付与する。 例) #mycode2(){{ 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 }} #html(</div>) // テストコードの基本形 ** assert の書き方 [#x11919d2] #html(<div style="padding-left:10px;">) https://docs.pytest.org/en/latest/assert.html *** 基本形 [#t6cbd22c] #html(<div style="padding-left:10px;">) #mycode2(){{ assert 真偽値(※) [, エラーメッセージ] }} ※ True の時に成功となる。 #html(</div>) *** 文字列マッチング [#w2ac605a] #html(<div style="padding-left:10px;">) #mycode2(){{ assert 'match string' in str(result) }} #html(</div>) *** 例外を期待する場合 [#v34ef498] #html(<div style="padding-left:10px;">) #mycode2(){{ with pytest.raises(ZeroDivisionError): 1 / 0 }} &br; #mycode2(){{ with pytest.raises(ValueError, match=r'.* 123 .*'): myfunc() }} #html(</div>) #html(</div>) // assert の書き方 ** fixtureの利用(テスト前後に行う処理) [#v69ba4d3] #html(<div style="padding-left:10px;">) fixtureを使用してテストの前後に行う処理を指定する事ができるので、これを利用してテストデータの投入などの行うのが良い。 ※conftest.py をうまく利用すれば、パッケージ単位で共通して利用するテストデータ等も用意できるので無駄が省ける。 *** scope について [#q71d9faf] #html(<div style="padding-left:10px;">) #html(<div style="display:inline-block;vertical-align:top">) fixture に scope を指定する事で、どの単位の前処理、後処理なのか明示する事ができる。 |scope|説明|h |session|テスト全体の前後処理| |function|各テストfunction毎に行う前後処理| |class|各テストclass毎に行う前後処理| |module|各テストモジュール毎に行う前後処理| |package|各テストパッケージ毎に行う前後処理| #html(</div>) #html(<div style="display:inline-block;vertical-align:top;padding-left:20px;">) 例) #mycode2(){{ 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("関数の後処理") }} #html(</div>) #html(</div>) // scope について end *** yield について [#x1f624c2] #html(<div style="padding-left:10px;">) fixture 内に yield を記述する事で、テスト後に行う処理を記述する事が出来る。 yield が記述されていないと、全てテストの前に実行されてしまうので注意が必要。 #mycode2(){{ @pytest.fixture(scope='function', autouse=True) def module_fixture(): print("関数の前処理") yield print("関数の後処理") }} #html(</div>) *** fixture からパラメータを受け取る [#ta69f232] #html(<div style="padding-left:10px;">) yield に引数を付けて実行すると、テストコード側で yield の引数を受け取る事ができる。 セットアップコードで生成したインスタンス等を利用してテストコードを実行したい場合等に利用できる。 #mycode2(){{ 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}') }} #html(</div>) *** 実行する fixture をテストコード毎に指定する [#l55b07d9] #html(<div style="padding-left:10px;">) 以下の例では、test_fixture_sample2 の時のみ、前処理、後処理として fixture_for_sample2 が実行される。 #mycode2(){{ 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') }} #html(</div>) #html(</div>) // テストの前後に行う処理 ** mark の利用 [#b678c566] #html(<div style="padding-left:10px;">) *** 同じテストを値を変えて行う [#gfdb5035] #html(<div style="padding-left:10px;">) pytest.mark.parametrize を使用すると、同じメソッドのテストを引数を得変えてテストする事ができる。 #html(<div style="display:inline-block;">) pytest.mark.parametrize を使用しない場合 #mycode2(){{ 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) }} #html(</div>) #html(<div style="display:inline-block;padding-left:20px;">) pytest.mark.parametrize を使用する場合 #mycode2(){{ 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) }} #html(</div>) #html(</div>) *** テストをグループ化する [#sc0ef4e4] #html(<div style="padding-left:10px;">) pytest.mark に続いて任意の名前を付けておくと、テストコードをグループ化する事ができ、グループ毎にテストを実行する事ができる。 #mycode2(){{ 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 のテストコードのみ実行する ) #myterm2(){{ mytest -m service1 -v }} #html(</div>) #html(</div>) // mark の利用 end ** Tips [#me3d48f1] #html(<div style="padding-left:10px;">) *** 標準出力に表示する [#o9a2ab13] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -s }} #html(</div>) *** 処理時間を計測する [#ic092ee4] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest --durations=0 pytest -v --duration=0 }} #html(</div>) *** テストケース別に結果を表示する [#s0422d2a] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -v }} #html(</div>) *** 前回NGだったケースだけテストする [#l20f53f6] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -v --lf }} #html(</div>) *** 前回NGだったケースからテストする [#bfed747f] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -v --ff }} #html(</div>) *** 遅いテストケースを見つける [#q5255543] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -v --duration=5 }} #html(</div>) *** テストのログをファイルに出力する [#sf30545f] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -v --result-log=tests/log.txt }} #html(</div>) *** カバレッジを確認する [#ad801cfa] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -v --cov=ディレクトリ }} #html(</div>) *** HTML形式のカバレッジレポートを出力する [#g6612da7] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -v --cov=ディレクトリ --cov-report=html }} #html(</div>) *** テストで実行されなかった行を表示する [#vbe41a0d] #html(<div style="padding-left:10px;">) #myterm2(){{ pytest -v --cov=ディレクトリ --cov-report=term-missing }} #html(</div>) #html(</div>) // tips end