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/
mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
src と tests を分ける場合で src フォルダに PATH を通すには、環境変数 PYTHONPATH を利用するか、テストコード全体の前処理(conftest.py) で sys.path に追加する。
※ テストコード自体の検索PATH は pytest.ini または pytest コマンドオプションの testpaths で指定する。
export PYTHONPATH=./src
import sys import os sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src/"))※ conftest.py については後述。
・ src 直下には __init__.py は置かない。
・ tests 直下には __init__.py を置く。
src/
__init__.py ... src直下には置かない
mypkg/
myfunc.py
tests/
__init__.py ... tests直下には置く
mypkg
test_myfunc.py
tests 配下のパッケージ/モジュール名のフルPATHが src 配下と全く同じになってしまうと、src 側のパッケージ配下のモジュールがうまく認識されない模様。( ImportError になる )
なので、tests 配下に __init__.py をおいて全てのテストコードのパッケージの頭には必ず tests. が付くようにする。
※よく見るとドキュメントでも tests ディレクトリ直下には __init__.py が置かれている。
https://docs.pytest.org/en/latest/goodpractices.html#choosing-a-test-layout-import-rules
イメージ)
src配下 | mypkg.myfunc |
tests配下 | tests.mypkg.myfunc |
テストコードの前処理、後処理を行う仕組みとして fixture が用意されているが、fixture は conftest.py として別ファイルに切り出す事ができる。
また、conftest.py は各フォルダ毎に配置する事ができるので、fixture の scope=session 等と組み合わせると、各フォルダ単位で行う前後処理を記述する事ができる。
※フォルダ単位で用意する共通のテストデータの投入などはここで行うと効率的。
tests/
__init__.py
conftest.py ... テスト全体にかかるセットアップコード
mypkg1
__init__.py
conftest.py ... mypkg1 配下のテスト用のセットアップコード
test_myfunc1.py
test_myfunc2.py
mypkg2
__init__.py
conftest.py ... mypkg2 配下のテスト用のセットアップコード
test_myfunc3.py
test_myfunc4.py
以下の命名規則に従っていれば、テストコードとして認識される。
例)
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
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を使用してテストの前後に行う処理を指定する事ができるので、これを利用してテストデータの投入などの行うのが良い。
※conftest.py をうまく利用すれば、パッケージ単位で共通して利用するテストデータ等も用意できるので無駄が省ける。
fixture に scope を指定する事で、どの単位の前処理、後処理なのか明示する事ができる。
scope | 説明 |
session | テスト全体の前後処理 |
function | 各テストfunction毎に行う前後処理 |
class | 各テストclass毎に行う前後処理 |
module | 各テストモジュール毎に行う前後処理 |
package | 各テストパッケージ毎に行う前後処理 |
例)
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("関数の後処理")
fixture 内に yield を記述する事で、テスト後に行う処理を記述する事が出来る。
yield が記述されていないと、全てテストの前に実行されてしまうので注意が必要。
@pytest.fixture(scope='function', autouse=True) def module_fixture(): print("関数の前処理") yield print("関数の後処理")
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}')
以下の例では、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')
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 のテストコードのみ実行する )
mytest -m service1 -v
pytest -s
pytest --durations=0 pytest -v --duration=0
pytest -v
pytest -v --lf
pytest -v --ff
pytest -v --duration=5
pytest -v --result-log=tests/log.txt
pytest -v --cov=ディレクトリ
pytest -v --cov=ディレクトリ --cov-report=html
pytest -v --cov=ディレクトリ --cov-report=term-missing