Pytest 利用ノート

Python のテストフレームワークとして著名かつ人気のある pytest に関するノートだ。

概要

The pytest framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries.

インストール・更新・アンインストール

複数人で共用するプロジェクトの開発環境に pytest をインストールする事例では、そのプロジェクトの定める手順に従え。README や pyproject.toml を読めば判明する。

自分が所有する作業用仮想環境に pytest をインストールするならば、愛用している仮想環境ツールがインストールコマンドを実装している場合にはそれを使え。私ならば Miniconda であるから、例えば次のようにする:

現在の conda 仮想環境に pytest をインストールする
conda install -c conda-forge pytest

仮想環境ツールがインストールコマンドを持っていない場合には、対象仮想環境が有効であることを確認してから pip を使え:

昔ながらの pip による pytest インストール手順例
pip install pytest

インストール手順の説明は以上だ。pytest の更新、アンインストールの手順は、対応する条件におけるインストール手順に合致する手順を選べ。例えば conda を使っているのならば conda uninstall pytest を走らせる、というようにだ。

See also

Hatch 利用ノート

必要になれば自動でインストールするツールを使うのがいちばん楽だ。

Miniconda 利用ノート

仮想環境管理管理ツール。

構成・カスタマイズ

Many pytest settings can be set in a configuration file, which by convention resides in the root directory of your repository.

Pytest はプロジェクトごとに構成ファイルを設ける流儀であり、ユーザー構成ファイルは考えなくてよい。この節の目的は終わった。

何かの過渡期であるようで、ファイル書式が複数ある。プロジェクトのルートディレクトリーに次のファイルのいずれかが置いてあれば、それが考慮される(この説明は厳密には正しくないが、理解のためにそう記した):

  • pytest.ini

  • pyproject.tomltool.pytest.ini_options 区画

  • tox.inipytest 区画

  • setup.cfgtool:pytest 区画

今の時代に INI ファイルは書きたくない。私は普通は pyproject.toml を用いる。次のようなコードを与える:

pyproject.toml における pytest 構成記述例
[tool.pytest.ini_options]
addopts = "--color=yes --showlocals --strict-markers"
asyncio_mode = "strict"
doctest_optionflags = [
  "NORMALIZE_WHITESPACE",
  "ELLIPSIS"
]
filterwarnings = [
  "error",
]
log_cli_level = "INFO"
markers = [
  "slow",
]
minversion = "8.0"
python_files = [
  "test_*.py",
  "__init__.py",
]
testpaths = "tests"
tmp_path_retention_policy = "failed"
xfail_strict = true

オプションの名称と型は pytest --help から確認可能。

使用方法・コツ

全般

Todo

  • Hatch を利用することで pytest を間接的にインストールする

    • Pipfile

    • pyproject.toml

  • 自作 marks

  • doctest

  • 前回失敗したエラーからテストを再開

  • 報告書

  • ログ

  • プラグイン

他のフレームワークから移行するコツ

標準テストモジュール unittest を使って構築したテスト一式がある場合、pytest に適合させるための作業は本質的には発生しない。コマンドラインで起点となるパスと PYTHONPATH を(上述の構成ファイルに記述する方式が望ましい)適当に指定すれば pytest は動作する。

Nose から移行する場合、nose2pytest を試せ。初回は学習目的で、敢えて手作業で移行するのも考えられる(時間的余裕が十分ある場合に限る)。

それでも unittest ベースのコードを pytest 様式に書き換える場合

TestCase のサブクラスにテストメソッドがあるだけのテストを書き換えるのは容易い:

  • テストモジュールに import pytest を加えるのが普通。

  • self.assertXXXX 系メソッド呼び出しをすべて assert 文に書き直す。それに伴い、引数リストを Python 文の形に書き直す。例えば:

    • self.assertEqual(x, y)assert x == y

    • self.assertIs(x, y)assert x is y

    • self.assertIn(x, y)assert x in y

    • self.assertRaises(Err, f, args)with pytest.raises(Err, matches="..."): f(args) and more

  • サブクラス定義の行を取り払い、インデントを一段浅くする。

  • メソッドだったものの第一引数 self を取り除く。フリー関数に書き直すため。

  • メソッド setUptearDown を実装していたテストクラスでは、この資源管理処理を後述するコツのように書き換える。

  • unittest.mock.{Magic,}Mockunittest.mock.patch を用いたコードを書き直す。例えば pytest 組み込みオブジェクト monkeypatch を用いたものに書き換える。機能の一部は代替不能かもしれない。

テストコードに関するコツ

テストコードに関するコツを記していくが、以下の事項については既知とする:

  • pytest.fixture の概念と適用方法。例えば組み込み fixture は明示的インポートなしで参照可能だ。

  • pytest.mark

  • monkeypatch

以下のコード例では通常 import pytest を必要とする。

資源の初期化と解放を管理する fixture では yield を使え

初期化と解放の両方が必要な fixture では return ではなく yield で資源を呼び出し元に引き渡せ。

RAII 実装例
@pytest.fixture
def resource(args):
    res = acquire_resoure(args)
    yield res
    release_resource(res)

専用 context manager がある場合には、上の関数定義が with ブロックになり、最後の文が yield res になるだけだ。本質的には同じこととなる。

テストディレクトリーに conftest.py を置け

ファイル conftest.py を用意し、テストモジュールのテスト関数がよく用いる fixture 関数定義をここで行え。さらに、テストモジュールが階層を形成している場合にはサブディレクトリーにもファイル conftest.py を置くことが有利である場合がある。サブディレクトリーにあるテスト機能に特化するように fixture をこのファイルで上書きすることが可能だ。

標準ストリームの内容を扱うのに capsys を使え

組み込み fixture である capsys から sys.stdout and/or sys.stderr の内容と同じものを読み取ることが可能だ。これは標準出力に対してしか出力しない機能をテストするときや、エラー出力をテストするときに有用だ。要点は:

  • この fixture は pytest 組み込みであるため、テスト関数の引数リストに即列挙可能だ。

  • capsys.readouterr() は標準出力と標準エラー出力の内容と同じ文字列の対を返す。それぞれを参照するには out, err を指定する。

capsys 利用例
def test_dump(capsys):
    dump(["C808DA", "12", "1389"])
    actual = capsys.readouterr().out.split("\n")
    assert actual[0].startswith("C8/08DA:")

送出される例外の型をテストするだけならば pytest.mark.xfail を使え

テスト関数を xfail で装飾するだけで済む。その raises キーワード引数に期待する例外型を指定しろ。

次の例は関数 confdir_home が特定の条件下(詳細割愛)で例外 ConfigNotFoundError を送出することを試験するものだ:

mark.xfail 利用例
@pytest.mark.xfail(raises=ConfigNotFoundError)
def test_confdir_home_error(monkeypatch):
    with monkeypatch.context():
        monkeypatch.setattr(os.environ, "get", lambda _: "")
        monkeypatch.setattr(pathlib.Path, "home", lambda: pathlib.Path("/dev/null"))
        confdir_home()

テスト実行結果では当該テストに印 x がつく。

未完成のテストには pytest.mark.skip を付けろ

ソフトウェア開発の性質上、テスト関数は完成しているが、本体機能のほうが未完成という場合が普通に生じる。そのような場合には次の例にようにしておけばよい:

mark.skip 利用例
@pytest.mark.skip(reason="This feature has been not yet completed")
def test_feature_not_yet_completed(): ...

実行条件が付くテストには pytest.mark.skipif を付けろ

前項の系だが、先行する開発機能があるなど、実行環境次第で成否が決まるテストには skipif を適用しておくと良さそうだ。

mark.skipif 利用例
@pytest.mark.skipif(not check_config(), reason="No configuration provided")
def test_confdir_home(): ...

データ差し替えテストには pytest.mark.parametrize を使え

テスト対象関数を、実引数だけ変えて何度か呼び出すようなテスト関数があるとする。そういう場合には pytest.mark.parametrize で decorate すると保守がし易くなる。そればかりでなく、テストケースの水増しにも有効だ。簡単な例でそれを示す:

mark.parametrize 使用前
def test_is_even():
    for i in range(0, 10, 2):
        assert is_even(i)

これを次のように書き換えると、テスト項目数が 1 から 5 に増える:

mark.parametrize 使用後
@pytest.mark.parametrize("number", range(0, 10, 2))
def test_is_even(number):
    assert is_even(number)

まともな decorator なので重ねがけができることに注意:

mark.parametrize 重ねがけの実例
@pytest.mark.parametrize(
    "data",
    (
        (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06),
        b"\x00\x01\x02\x03\x04\x05\x06",
    ),
)
@pytest.mark.parametrize(
    "index,length,expected",
    (
        (0, 1, 0x00),
        (1, 1, 0x01),
        (0, 2, 0x0100),
        (6, 2, 0x0006),
        (0, 3, 0x020100),
        (5, 3, 0x000605),
        (6, 3, 0x000006),
    ),
)
def test_get_int(data, index, length, expected):
    assert get_int(data, index, length) == expected

一括適用に pytestmark を指定しろ

あるテストモジュール(またはテストクラス)が含むテスト関数全てを pytest.mark.skip, pytest.mark.xfail, .etc で適用するには、pytest 組み込みオブジェクト pytestmark に値を代入するのが手軽だ:

pytestmark 使用例
# In a test module

from mypackage.config import check_config

pytestmark = pytest.mark.skipif(
    check_config() is None, reason="No configuration provided"
)

この右辺自体を複数のテストモジュールで使用したい場合には、それを __init__.py などで別のオブジェクト (e.g. requires_config) に代入しておき、各モジュールからそれを import して pytestmark = requires_config などとすればコード量がさらに抑えられるだろう。

知っていると便利なコマンドラインオプション

重要なオプション設定は構成ファイルに記述するので、ここに挙げるのは実行時にしか指定することがないオプションとする。

テストを実行しないコマンドを先に挙げる:

pytest --version

pytest 本体と、もしあればプラグインのバージョンを出力する。

pytest --help

pytest インターフェイス仕様記述を出力する。構成ファイルの項目名を確認することも可能だ。

pytest -q

実行結果を許される範囲でコンパクトに出力する。ドットなどが縦横に並んで表示され、定型的文言は省かれる。

pytest --collect-only

実行テスト一覧を階層的に表現して出力する。後述する実行コマンドに付加して用いる。

pytest --fixtures

使用可能な fixtures 一覧を、その関数 docstring と共に出力する。基本的には組み込み、プラグイン、自作の順番に fixtures が並ぶようだ。

pytest --fixtures-per-test

テスト関数それぞれに対して仕込まれる fixtures を一覧する。言い換えると、テスト関数の引数リストに現れる仮引数がすべて示される。この出力にも docstring が使われる。

pytest --markers

使用可能な markers 一覧を出力する。

テスト動作を調整するオプションを指定したコマンド:

pytest -k keyword

文字列 keyword を部分文字列とする名前のテストに限定して実行する。アルファベットの大文字小文字は区別しない。実際はもっと高機能だが、とりあえずこれだけ憶えておく。文字列を適宜引用符で囲むのがよい。

利点:テスト対象ファイル名を憶えていなくて済む。

pytest -m marker-spec

テストのうち、その @pytest.markmarker-spec に合致するものしか実行しない。

pytest --pdb

エラー時または例外 KeyboardInterrupt が発生した場合に対話型 Python デバッガーを起動させる。

Note

利用者ノート

再テスト順序指定オプションやログオプションなど、まだまだ機能がある。

コマンドライン引数

コマンドライン引数を何も指定しない場合、一般的にコマンド pytest は作業ディレクトリーとその下位にある test_*.py または *_test.py というファイル名の全てのテストを実行する。特定のテストを実行するには、例えば次のいずれかのパターンのコマンドを組み立てる:

pytest tests/

ディレクトリー tests/ およびその下位にあるテストモジュールのテスト全てを対象とする。

pytest tests/test_mod.py

モジュール tests/test_mod.py にあるテスト全てを対象とする。

pytest tests/test_mod.py::test_func

モジュール tests/test_mod.py にあるテスト関数 test_func を唯一のテスト対象とする。

pytest tests/test_mod.py::test_func[x1,y2]

モジュール tests/test_mod.py にあるテスト関数 test_func を唯一のテスト対象とし、さらにその引数指定を x1,y2 とする。

pytest tests/test_mod.py::TestClass

モジュール tests/test_mod.py にあるテストクラス TestClass にあるテスト全てを対象とする。

pytest tests/test_mod.py::TestClass::test_method

モジュール tests/test_mod.py にあるテストクラス TestClass にあるメソッド test_method を唯一のテスト対象とする。

Note

利用者ノート

ツール等を経由して pytest を実行する場合、パスを入力するのにファイルパスに対するタブ補完が効かずに、面倒である場合があることに注意。そういう場合は前項で見たオプション -k keyword を用いる。

環境変数

さまざまな環境変数が pytest の挙動に影響するが、基本的には利用しない。

CI, BUILD_NUMBER

これらの環境変数は開発者が手動で declare するものではない。

どちらかでも設定されていれば、pytest は自身が CI 環境で実行されているものとして振る舞う。例えば、pytest.fail などの概要情報の出力が端末ウィンドウの横幅に切り詰められなくなる。

PYTEST_ADDOPTS

構成ファイルのオプション addopts の環境変数版だ。オプションを構成ファイルで指定するのが通例であり、用いない。

PYTEST_VERSION

この環境変数は pytest 開始時に定義される。テストを実行したのがコマンドラインからであるのか否かを判定するのに用いられるらしい。

PYTEST_CURRENT_TEST

pytest はテストを実行する際にこの環境変数を設定する。テストが固まったときにこの値を参照するようにすることで、どのテストで詰んでいるのかの手がかりが得られる。

公式文書では psutil.pids() の各プロセスに対して environ() でこの環境変数の存在、設定値を確認するという処理例が挙げられている。

PYTEST_DEBUG

宣言してから pytest を実行するとトレースとデバッグ情報をも出力する。

出力量が厖大ゆえ、使うのはテスト対象を絞りに絞ってからにしろ。

PYTEST_DEBUG_TEMPROOT

一時ファイル系 fixtures が作成する一時ディレクトリーの基点となるパス。

コマンドラインオプション --basetemp に相当する。よって使わない。

PYTEST_DISABLE_PLUGIN_AUTOLOAD

宣言されている場合に、プラグインの自動ロードが無効になる。明示的に指定されたプラグインはロードされる。

PYTEST_PLUGINS

プラグインとしてロードしたいモジュールを CSV 形式で指定すると、そうなる。

PYTEST_THEME

コード出力に使用する Pygments 利用ノート スタイルを設定すると、pytest の出力にそれが反映される。

こんな凝ったオプションは使わない。

PYTEST_THEME_MODE

PYTEST_THEMEdarklight のどちらかに設定する。

PY_COLORS

値を 1 に設定すると pytest は端末出力で色を使う。値を 0 に設定するとそうはしない(テスト結果記号が地の文字色と同じくなる)。

Note

pytest は環境変数 NO_COLOR を考慮する。

評価

これから個人開発の Python パッケージには pytest をなるべく導入しよう。次の機能が気に入った:

  • テストの「証拠集め」を Python 標準の assert 一丁で賄う姿勢。

  • Fixtures の使い勝手。再利用性がすこぶる良い。

  • pytest.mark.paramtrize でテストケースを大幅に実量以上に見せかけるのが容易であること。

  • pytestmark

資料集

何しろ人気パッケージであり、インターネットを検索すると高品質の記事がいたるところにある。それらをすぐに参照できるようにして整理しておくほうが、私が独りよがりな何やらを書き殴るよりもはるかに良いだろう。

pytest documentation

公式文書。全ページに目を通せ。掲載コードを可能な限り検証しろ。紙幅の都合上、動作に足りないコード片があるものの、理解できている読者ならば補える。

Effective Python Testing With pytest

Real Python 内解説記事。

Pytest — A beginner Guide

入門記事で xfailskip に触れているものは珍しい。

Pytest vs Unittest: A Comparison

両者を徹底的に比較考量している。長所と短所を列挙し、各項目について双方の性質を述べている。記事の造りが堅実であり、すごく参考になる。

Pytest vs. Unittest: Which Is Better?

こちらも pytest と unittest の比較記事で、コードを示している。

Pytest: Getting started with automated testing for Python

CircleCI にある文書。単語 streamline がよく出てくることから、このテストフレームワークに期待される重要な性質が見えてくる。

What Is pytest: A Complete pytest Tutorial With Best Practices

Selenium を使ったアプリケーションのテストを pytest で実施するチュートリアルを含む記事(私はまだ試していない)。序盤の統計データも興味深い。

以上のほかにも優等記事が世にあるはずだ。

I want to use stdin in a pytest test

Stack Overflow より。