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/
 mypkg/
  __init__.py
  app.py
  view.py
tests/
 __init__.py
 foo/
  __init__.py
  test_view.py
 bar/
  __init__.py
  test_view.py

src フォルダのPATHの通し方

src と tests を分ける場合で src フォルダに PATH を通すには、環境変数 PYTHONPATH を利用するか、テストコード全体の前処理(conftest.py) で sys.path に追加する。
※ テストコード自体の検索PATH は pytest.ini または pytest コマンドオプションの testpaths で指定する。

  • 環境変数で行う場合
    export PYTHONPATH=./src
    
  • テストコードの前処理で行う場合
    tests/conftest.py
    import sys 
    import os
    
    sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src/"))
    
    ※ conftest.py については後述。

__init__.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

セットアップコードと conftest.py

テストコードの前処理、後処理を行う仕組みとして 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

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を使用してテストの前後に行う処理を指定する事ができるので、これを利用してテストデータの投入などの行うのが良い。
※conftest.py をうまく利用すれば、パッケージ単位で共通して利用するテストデータ等も用意できるので無駄が省ける。

scope について

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("関数の後処理")

yield について

fixture 内に yield を記述する事で、テスト後に行う処理を記述する事が出来る。
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

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

トップ   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS