pytest でテストをスキップ、コマンドオプション引数でスキップする

pytest でテストをスキップ、コマンドオプション引数でスキップする

pytest-skip でテストをスキップする

Skipping | https://docs.pytest.org/en/reorganize-docs/new-docs/user/skipping.html

テストコードが特定の環境でしか動かない場合(例えばWindows環境では動かない場合)や、Pythonのバージョンに依存したテストコードがある場合、実行に時間がかかるので普段のテストでは実行したくないなど、様々な理由によって一部のテストを実行しないようにスキップしたい場面があります。

pytest では一部のテストメソッドやテストクラスをスキップするマーカーが用意されおり、これを使えばテスト実行時にスキップすることが可能です。

skipif で条件を動的に指定してテストをスキップ

@pytest.mark.skipif で条件を指定することで、テストを実行するかスキップするかを動的に決定できます。

例えばわかりやすく以下のように、スキップするテストとしないテストを実装してみます。

import pytest

@pytest.mark.skipif(True, reason="[スキップする理由]")
def test_function_1():
    pass

@pytest.mark.skipif(False, reason="[スキップする理由]")
def test_function_2():
    pass

pytest -v でテスト結果を詳細に確認してみると以下のようになります。

$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.8.3, pytest-6.2.2, py-1.10.0, pluggy-0.13.1 -- /home/tm/.pyenv/versions/3.8.3/bin/python3.8
cachedir: .pytest_cache
rootdir: /mnt/c/work/pytest-sample
plugins: mock-3.5.1
collected 2 items

tests/test_my_code.py::test_function_1 SKIPPED ([スキップする理由])      [ 50%]
tests/test_my_code.py::test_function_2 PASSED                            [100%]

========================= 1 passed, 1 skipped in 0.03s =========================

test_function_1 がスキップされていることがわかります。reason にスキップする理由を書いておけば確認できます。ここでは固定でスキップするかどうかのフラグを指定しましたがもちろん動的に指定してスキップするかどうかを実行時に判断することも可能です。

Python の2系では実行しないようにスキップ

Skipping

上記ページからの引用ですが、例えば Python3系を必要とするテストコードでは以下のように @pytest.mark.skipif でバージョン情報を参照した結果を条件として指定します。

import sys
@pytest.mark.skipif(sys.version_info < (3, 0),
                    reason="requires Python3")
def test_function():
    ...

このテストを Python2系で実行するとスキップされます。

テストを無条件でスキップする

skipif ではなく skip とすることで条件の指定なしで強制的にスキップすることができます。

@pytest.mark.skip(reason="テストがうまくいかないのでこのテストは無条件でスキップします。")
def test_sample():
    pass

今はテストできないけど原因が解決できればテストしたい場合などに利用するイメージでしょうか。

テストクラスをまとめてスキップ

import pytest

@pytest.mark.skip(reason="[スキップする理由]")
class TestClass:
    def test_function_1():
        pass

    def test_function_2():
        pass

スキップのデコレータをテストクラスに付与すればテストクラス内のテストコード全体がスキップされます。

スキップマーカーを使いまわす

python3_only = pytest.mark.skipif(sys.version_info < (3, 0),
                                  reason="requires Python3")

@python3_only
def test_function1():
    pass

スキップデコレータを使いまわす場合は変数として参照することが推奨されるみたいです。

コマンドのオプション引数の指定で動的にスキップ対象を決める

コマンドラインのオプション引数で実行するテストの範囲を指定したいです。例えば実行に時間がかかるテストがあったとして、--runslow が指定されている場合のみスキップせずに実行するということを実装したいです。

以下のドキュメントページにまさにそのサンプルコードが載っています。

Basic patterns and examples — pytest documentation

# content of conftest.py

import pytest


def pytest_addoption(parser):
    parser.addoption(
        "--runslow", action="store_true", default=False, help="run slow tests"
    )


def pytest_configure(config):
    config.addinivalue_line("markers", "slow: mark test as slow to run")


def pytest_collection_modifyitems(config, items):
    if config.getoption("--runslow"):
        # --runslow given in cli: do not skip slow tests
        return
    skip_slow = pytest.mark.skip(reason="need --runslow option to run")
    for item in items:
        if "slow" in item.keywords:
            item.add_marker(skip_slow)
# content of test_module.py
import pytest


def test_func_fast():
    pass


@pytest.mark.slow # <- 独自のマーカー
def test_func_slow():
    pass

pytest ではカスタマイズ用に幾つかのイベントをフックすることが可能です。これらを組み合わせてでやりたいことが実現できます。

  1. コマンドラインのオプション引数を解析する
  2. 独自のマーカーをテスト開始前に生成する
  3. 独自マーカーをテストにデコレータで指定する

手順的には上の通りです。順に見ていきます。

pytest_addoption: コマンドラインのオプション引数を解析する

pytest_addoption というフックを使えば、パーサーを参照してオプション引数を解析できます。

pytest_addoption はテスト開始前に1度だけ実行されます。引数の parser は標準ライブラリの argparser と同じようなインターフェースでコマンドラインのオプション引数をパースできます。上記ドキュメントのURLを参照してください。

def pytest_addoption(parser):
    """
    コマンドラインのオプションを解析し
    --runslow があれば True, なければ False を
    変数として保持する
    """
    parser.addoption(
        '--runslow', 
        action='store_true', 
        default=False, 
        help='run slow tests'
    )

action='store_true' はオプションの指定があるかないかでフラグ結果を保持します。argperser と同じ使用感ですね。

ここで解析した結果を後続のイベントで使用します。

pytest_configure: 独自マーカーの説明を追加

pytest_configure は初期構成を実行できるようにするフックです。コマンドラインオプションの解析(pytest_addoption)実行後に呼び出されます。

pytest --markers で使用可能なマーカーを一覧できます。そこに独自マーカーの説明を追加しておくとわかりやすいので追加します。

def pytest_configure(config):
    '''
    $ pytest --markers
    上記コマンドで参照できるマーカーの説明を追加します。
    '''
    config.addinivalue_line('markers', 'slow: 実行に時間がかかるテストのマークです。')

もちろん無くても動作します。が、以下のようにマーカーの使い方を確認した時に説明があれば利用者側にはわかりやすいので追加しておくとよいでしょう。

$ pytest --markers | grep slow
@pytest.mark.slow: 実行に時間がかかるテストのマークです。

pytest_collection_modifyitems: 独自マーカーがあればスキップする設定

pytest_collection_modifyitems はテスト対象の収集完了後に呼び出されるフックです。テスト対象を並べ替えたりフィルタリングしたりできます。

ここでは独自マーカーが設定されたテスト対象に対してスキップするような設定を行います。

def pytest_collection_modifyitems(session, config, items):
    # --runslow オプションが無ければ無視します。
    if config.getoption('--runslow'):
        return
    
    # 独自のスキップマーカー
    skip_slow = pytest.mark.skip(reason='実行にはオプション --runslow が必要です。')

    # 全テスト対象のメソッドを走査
    for item in items:
        # 'slow'マーカーがあればスキップマーカーを付与
        if 'slow' in item.keywords:
            item.add_marker(skip_slow)

pytest_collection_modifyitemsconfig という引数でコマンドラインオプションの解析結果を参照できます。ここでは config.getoption--runslow が指定されているかを判定し、指定がなければ処理せずに終了します。

引数の items は収集されたテスト対象です。これらをすべて走査して、マーカーに @pytest.mark.slow という独自マーカーが設定されているかチェックし、設定があればスキップマーカーを追加してスキップするようにします。

以上の手順で、コマンドラインオプションの指定結果を使って、動的にスキップ対象のテストを指定できました。

コード全体

これまでのコード内容をまとめたものを掲載します。プロジェクト構成はこんな感じです。

$ tree
.
├── conftest.py
└── test_sample.py

conftest.py

import pytest


def pytest_addoption(parser):
    '''
    コマンドラインのオプションを解析し
    --runslow があれば True, なければ False を
    変数として保持する
    '''
    parser.addoption(
        '--runslow', 
        action='store_true', 
        default=False, 
        help='実行時間がかかるテストを実行します。'
    )


def pytest_configure(config):
    '''
    $ pytest --markers
    上記コマンドで参照できるマーカーの説明を追加します。
    '''
    config.addinivalue_line('markers', 'slow: 実行に時間がかかるテストのマークです。')


def pytest_collection_modifyitems(session, config, items):
    # --runslow オプションが無ければ無視します。
    if config.getoption('--runslow'):
        return
    
    # 独自のスキップマーカー
    skip_slow = pytest.mark.skip(reason='実行にはオプション --runslow が必要です。')

    # 全テスト対象のメソッドを走査
    for item in items:
        # 'slow'マーカーがあればスキップマーカーを付与
        if 'slow' in item.keywords:
            item.add_marker(skip_slow)

test_sample.py

import pytest

@pytest.mark.slow
def test_slow_func():
    pass

def test_sample():
    pass

上記テストを実行すると以下のようになります。

$ pytest -v
==================================== test session starts ====================================
platform linux -- Python 3.8.3, pytest-6.2.2, py-1.10.0, pluggy-0.13.1 -- /home/tm/.pyenv/versions/3.8.3/bin/python3.8
cachedir: .pytest_cache
rootdir: /mnt/c/work/pytest-sample
plugins: mock-3.5.1
collected 2 items

test_sample.py::test_slow_func SKIPPED (実行にはオプション --runslow が必要です。)   [ 50%]
test_sample.py::test_sample PASSED                                                [100%]

=============================== 1 passed, 1 skipped in 0.03s ================================

独自のマーカー @pytest.mark.slow を設定したテストのみスキップされ、その理由が確認できています。

以上。

参考URL

Pythonカテゴリの最新記事