2021-09-10

[Python] 初中級者のためのpytest入門

この記事は過去に自分が携わっていた案件のコードを理解するために書いたものです。

前は公式の日本語ドキュメントがあったんですが、迷宮に迷い込んだようです(404)

ちなみに英語ドキュメントは普通にあるので読める人はそっちを読んだほうがいいです。 pytest: helps you write better programs — pytest documentationhttps://docs.pytest.org/en/latest/

以下のようにインストールします。

$ pip install pytest
info
  • 2021年09月に以下のバージョンで確認しながら大幅に加筆・訂正を行いました。
  • pytest 6.2.5
  • Python 3.9.6

テストランナーとしてのpytest

pytestは簡単に始められます。フレームワークに依存していなければテストケースを置き換えなくても実行するだけでOKです。

手始めに以下のファイルを作成します。

実行結果

この状態でとりあえず実行してみましょう。

$ py.test ========================= test session starts ========================== b_test.py FFF. [ 50%] test_a.py .FFF [100%] =============================== FAILURES =============================== ________________________________ test_5 ________________________________ def test_5(): a = (1, 2, 3, 4, 5, 6) b = (1, 2, 4, 3, 5, 6) > assert a == b E assert (1, 2, 3, 4, 5, 6) == (1, 2, 4, 3, 5, 6) E At index 2 diff: 3 != 4 E Use -v to get the full diff b_test.py:4: AssertionError ________________________________ test_6 ________________________________ def test_6(): a = [1, 2, 3] b = (1, 2, 3) > assert a == b E assert [1, 2, 3] == (1, 2, 3) E Use -v to get the full diff b_test.py:9: AssertionError ________________________________ test_7 ________________________________ def test_7(): a = {'a': 1, 'b': 2, 'c': 3, 'd': None} b = {'a': 1, 'b': 4, 'c': 3} > assert a == b E AssertionError: assert {'a': 1, 'b':... 3, 'd': None} == {'a': 1, 'b': 4, 'c': 3} E Omitting 2 identical items, use -vv to show E Differing items: E {'b': 2} != {'b': 4} E Left contains 1 more item: E {'d': None} E Use -v to get the full diff b_test.py:14: AssertionError ________________________________ test_2 ________________________________ def test_2(): a = 1 b = 2 > assert a == b E assert 1 == 2 test_a.py:11: AssertionError ____________________________ Tekitou.test_3 ____________________________ self = <test_a.Tekitou testMethod=test_3> def test_3(self): a = 1 b = 3 > self.assertEqual(a, b) E AssertionError: 1 != 3 test_a.py:21: AssertionError ___________________________ TestMine.test_4 ____________________________ self = <test_a.TestMine object at 0x107ee3eb0> def test_4(self): a = 1 b = 4 > assert a == b E assert 1 == 4 test_a.py:27: AssertionError ======================= short test summary info ======================== FAILED b_test.py::test_5 - assert (1, 2, 3, 4, 5, 6) == (1, 2, 4, 3, ... FAILED b_test.py::test_6 - assert [1, 2, 3] == (1, 2, 3) FAILED b_test.py::test_7 - AssertionError: assert {'a': 1, 'b':... 3,... FAILED test_a.py::test_2 - assert 1 == 2 FAILED test_a.py::Tekitou::test_3 - AssertionError: 1 != 3 FAILED test_a.py::TestMine::test_4 - assert 1 == 4 ===================== 6 failed, 2 passed in 0.16s ======================

うん、わかりやすいですね。 (てかsetとfronzensetで比較結果同じなの知らなかった)

このとき「-v」や「-vv」を付けると差分が詳細表示されます。こんな風に..!

  • -v
  • -vv
  • E AssertionError: assert {'a': 1, 'b':... 3, 'd': None} == {'a': 1, 'b': 4, 'c': 3} E Omitting 2 identical items, use -vv to show E Differing items: E {'b': 2} != {'b': 4} E Left contains 1 more item: E {'d': None} E Full diff: E - {'a': 1, 'b': 4, 'c': 3}... E E ...Full output truncated (4 lines hidden), use '-vv' to show
  • E AssertionError: assert {'a': 1, 'b': 2, 'c': 3, 'd': None} == {'a': 1, 'b': 4, 'c': 3} E Common items: E {'a': 1, 'c': 3} E Differing items: E {'b': 2} != {'b': 4} E Left contains 1 more item: E {'d': None} E Full diff: E - {'a': 1, 'b': 4, 'c': 3} E ? ^ E + {'a': 1, 'b': 2, 'c': 3, 'd': None} E ? ^ +++++++++++

テスト対象

pytestのテスト対象は、具体的には Test~unittest.TestCase を継承したクラス か何にも属さない test_~ メソッドがテストケースと認識されます。

テスト対象を選択しないとカレントディレクトリ配下すべてがテスト対象となります。 すべてとは言っても test_~ , ~_test のファイルが対象となります。

つまり「test.py」や「tests.py」だと検出されないので注意してください。 テストファイルを引数に指定することで該当ファイルに含まれるテストケースだけに限定できます。

glob書式 を使えるので py.test test* といった書き方もできます。 また、-k を指定すると指定された文字列を含むテストケースだけが実行されます。 (おそらくこれが一番良く利用するオプションとなるでしょう)

$ py.test -k Mine ========================= test session starts ========================== test_a.py F [100%] =============================== FAILURES =============================== ___________________________ TestMine.test_4 ____________________________ self = <test_a.TestMine object at 0x10f0f1700> def test_4(self): a = 1 b = 4 > assert a == b E assert 1 == 4 test_a.py:27: AssertionError ======================= short test summary info ======================== FAILED test_a.py::TestMine::test_4 - assert 1 == 4 =================== 1 failed, 7 deselected in 0.10s ====================

メソッドにもクラスにも該当します。

setup.cfg

setup.cfgに記述することで使うオプションの固定やテスト対象を設定できます。 または pytest.ini, tox.ini にも記述できます。

[pytest] testpaths = . python_files = test_*.py python_classes = My python_functions = test minversion = 2.9 addopts = --maxfail=2 --showlocal --pdb

たとえば上記のようにすると次のようなルールになります。

  • カレントディレクトリの配下に存在する test*.py が対象とする
  • テストクラス名が My から始まるものを対象とする
  • テストメソッド名が test で始まるものを対象とする(これもglobが使えるっぽい)
  • pytestのバージョンが 2.9以上 であることを強制する
  • 実行時のオプションに --maxfail=2, --showlocal, --pdb を指定する(オプションは後述)

オプション

py.testは -v(差分詳細表示), -k(テストケース指定) 以外にもたくさんオプションがあります。

ただ、全部やるのはだるいので適当に選びます。

doctest-modules

doctest を実行するためのオプションです。テスト以外のモジュールであっても読み込まれて実行されます。

info
  • doctest とは 関数(メソッド) 内に書かれた docstring に書かれたテストのことです。

  • >>> の後ろに書かれた Pythonコードを実行し、結果を 文字列 として一致比較をします。

  • >>> def test1(): ... """ ... it must always return 1. ... >>> test1() ... 1 ... """ ... return 1 >>> def test2(): ... """ ... it must always return 2. ... >>> test2() ... 2 ... but ... # doctest を終わらせるには空行が必要 ... """ ... return 3 import doctest doctest.testmod() ********************************************************************** File "__main__", line 4, in __main__.test2 Failed example: test2() Expected: 2 Got: 3 ********************************************************************** 1 items had failures: 1 of 1 in __main__.test2 ***Test Failed*** 1 failures. TestResults(failed=1, attempted=2)

maxfail

一定数以上のテストケースが失敗した場合に終了となります。

$ py.test --maxfail=1 b_test.py F =============================== FAILURES =============================== ________________________________ test_5 ________________________________ def test_5(): a = (1, 2, 3, 4, 5, 6) b = (1, 2, 4, 3, 5, 6) > assert a == b E assert (1, 2, 3, 4, 5, 6) == (1, 2, 4, 3, 5, 6) E At index 2 diff: 3 != 4 E Use -v to get the full diff b_test.py:4: AssertionError ======================= short test summary info ======================== FAILED b_test.py::test_5 - assert (1, 2, 3, 4, 5, 6) == (1, 2, 4, 3, ... !!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!! ========================== 1 failed in 0.09s ===========================

「1」の場合は「-x」オプションと等価です。

pdb

テスト失敗時にPDBを起動します。

$ py.test --pdb >>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>>> -> assert a == b (Pdb) p a (1, 2, 3, 4, 5, 6) (Pdb) p b (1, 2, 4, 3, 5, 6) (Pdb) n F

余談ですが、この機能はnoseにもあるみたいですね。しらなかった。

showlocals

テスト失敗時に変数をローカル変数を表示します。 グローバル変数と比較の結果異なったとしてもグローバル変数は表示されないので注意してください。

$ py.test --showlocals def test_7(): a = {'a': 1, 'b': 2, 'c': 3, 'd': None} b = {'a': 1, 'b': 4, 'c': 3} > assert a == b E AssertionError: assert {'a': 1, 'b': 2, 'c': 3, 'd': None} == {'a': 1, 'b': 4, 'c': 3} E Common items: E {'a': 1, 'c': 3} E Differing items: E {'b': 2} != {'b': 4} E Left contains 1 more item: E {'d': None} E Full diff: E - {'a': 1, 'b': 4, 'c': 3} E ? ^ E + {'a': 1, 'b': 2, 'c': 3, 'd': None} E ? ^ +++++++++++ a = {'a': 1, 'b': 2, 'c': 3, 'd': None} b = {'a': 1, 'b': 4, 'c': 3}

collect-only

テストケースだけあつめて、実行しないオプションです。

$ py.test --collect-only ========================= test session starts ========================== collected 8 items <Module b_test.py> <Function test_5> <Function test_6> <Function test_7> <Function test_8> <Module test_a.py> <Function test_1> <Function test_2> <UnitTestCase Tekitou> <TestCaseFunction test_3> <Class TestMine> <Function test_4> ====================== 8 tests collected in 0.02s ======================

tb

(トレースバックの略らしい)テスト結果の表示フォーマットです。

long
  • デフォルトの詳細なトレースバック形式
native
  • Python 標準ライブラリの形式
short
  • 短いトレースバック形式
line
  • 失敗したテストを1行表示

tb=line を使ってみます。

$ py.test --tb=line ========================= test session starts ========================== =============================== FAILURES =============================== /src/tests/tests3/b_test.py:4: assert (1, 2, 3, 4, 5, 6) == (1, 2, 4, 3, 5, 6) /src/tests/tests3/b_test.py:9: assert [1, 2, 3] == (1, 2, 3) /src/tests/tests3/b_test.py:14: AssertionError: assert {'a': 1, 'b':... 3, 'd': None} == {'a': 1, 'b': 4, 'c': 3} /src/tests/tests3/test_a.py:11: assert 1 == 2 /src/tests/tests3/test_a.py:21: AssertionError: 1 != 3 /src/tests/tests3/test_a.py:27: assert 1 == 4 ===================== 6 failed, 2 passed in 0.03s ======================

durations

テストの実行時間をプロファイリングします。多い順に表示してくれます。

まぁ今回は全部0秒なんですが。

$ py.test --durations=100 -vv ======================== slowest 100 durations ========================= 0.00s call b_test.py::test_7 0.00s setup b_test.py::test_5 0.00s call b_test.py::test_5 0.00s setup test_a.py::Tekitou::test_3 0.00s call b_test.py::test_6 0.00s setup b_test.py::test_8 0.00s setup test_a.py::test_1 0.00s setup b_test.py::test_6 0.00s setup b_test.py::test_7 0.00s setup test_a.py::TestMine::test_4 0.00s setup test_a.py::test_2 0.00s teardown test_a.py::TestMine::test_4 0.00s call test_a.py::test_2 0.00s teardown b_test.py::test_7 0.00s teardown b_test.py::test_5 0.00s call test_a.py::TestMine::test_4 0.00s call test_a.py::Tekitou::test_3 0.00s teardown b_test.py::test_8 0.00s teardown test_a.py::Tekitou::test_3 0.00s teardown test_a.py::test_2 0.00s teardown b_test.py::test_6 0.00s teardown test_a.py::test_1 0.00s call b_test.py::test_8 0.00s call test_a.py::test_1

テストコードに記述に係わるpytest

これまではテスト実行、つまりコマンドとしてpytestを使ってきましたが ここからはテストの中身に触れていきます。

フィクスチャ

フィクスチャをかんたんに説明するとテスト実行時にデコレータを設定した関数が実行され、その返却値がテストで参照できるというものです。 個人的にはpytestの中で最も重要な機能で、これを使わなければpytestを使う意味は半分くらいなくなってしまいます。

使い方自体は簡単ですが動きはちょっとマジカルなので、初めて触る方は抵抗を感じるかもしれません。 まぁとりあえずイメージを掴んでもらうために少し例を見てもらいましょう。

Django(DRF)アプリにて、セッションでログインしたユーザがAPIにアクセスするテストを考えてみました。

@pytest.fixture def user(): from .factories import UserFactory return UserFactory(name="ピチュー") # 実在の人物や団体とは関係ありません @pytest.fixture def user_client(user): from rest_framework.test import APIClient user_client = APIClient() user_client.force_authenticate(user=user) return user_client def test_get(user_client): res = user_client.post(f"/api/me/") assert res.data["name"] == "ピチュー" def test_patch(user, user_client): res = user_client.post(f"/api/me/", data={"name": "ピカチュウ"}, content_type="application/json") user.refresh_from_db() assert user.name == "ピカチュウ", "ピカチュウじゃないっす" # 実在の人物や団体とは関係ありません

今回のテストケースは test_get と test_patch です。 かんたんなので説明しなくてもわかるかもしれませんが、test_get はAPIで取得した自分の名前が ピチュー かどうか、 test_patch はAPIで更新した自分の名前が ピカチュウ になっているかどうかを確認しています。

@pytest.fixture デコレータを設定した関数はフィクスチャであり、 この例でいうと useruser_client がフィクスチャです。

user フィクスチャであれば、テスト実行時に UserFactory が実行され、実行結果のユーザオブジェクトが得られます。 フィクスチャ関数自身もフィクスチャを引数に取ることができるため、 user_client フィクスチャは初期化時に user フィクスチャから取得したユーザオブジェクトを使ってログイン済にしたクライアントを返却します。

テスト関数ではこれらのフィクスチャを引数に取ることで実行時にはその引数自身がフィクスチャの返却値に化けます。 というかpytestのテストランナーが差し替えてから実行します多分。 つまり、テスト実行時には user 引数は ユーザオブジェクトとなり、 user_client オブジェクトはログイン済みのクライアントオブジェクトとなります。

フィクスチャは指定された粒度(後述)で初期化されますが、同一テスト内では同じフィクスチャ名から同じオブジェクトが得られるという重要な性質があります。 例えば、 user_client フィクスチャが参照している user フィクスチャと、 test_patch 関数が参照している user は同じユーザオブジェクトとなります。 (そうでなければこのテストはPassしません)

マジカルだといったのは、テストケースにてどのフィクスチャを使うかは仮引数名に指定することになり、 通常のPythonプログラムを書く感覚とは異なるためです。

autouse

今回の例はテストケースの中で直接参照するため引数に指定しましたが、参照はしないが初期化されて欲しいものがでてきます。 その際に毎回指定するのは煩わしく感じることでしょう。

autouse をフィクスチャに指定することでフィクスチャの指定が不要になります。

たとえば、以下のように記述すると同じファイルに属するテストケース全てで2人のユーザが初期化されるようになります。

@pytest.fixture def setupUsers(autouse=True)): from .factories import UserFactory UserFactory(name="しずえ") # 実在の人物や団体とは関係ありません UserFactory(name="パックマン") # 実在の人物や団体とは関係ありません

スコープ

フィクスチャは指定された粒度で初期化されると言いました。 この粒度をスコープといいます。Pythonのスコープとは別物です。以下の種類があります。

session
  • テストランナー実行毎に1回呼び出される。
module
  • テストモジュール毎に1回呼び出される。
class
  • テストクラス毎に1回呼び出される。 テストケースがクラス構造になっていない場合は都度呼び出される。
function
  • テストケースメソッド毎に呼び出される。

デフォルトは function なので、全てのテストケースで実行されます。

言い換えれば function 以外のスコープを設定された フィクスチャは同じスコープに属する前のテストケースの結果に影響を受ける可能性があります。 初期化処理が重いだとか複数回実行してはいけないものでない限り、フィクスチャにスコープを指定する必要性はそれほどないでしょう。

スコープ指定と呼び出し回数を関係を確かめてみます。

  • test files
  • py.test test_scope* -s
  • ~~ 前略 ~~ test_scope1.py always session always module always module for scope1 always class always function test 34 .always function test 35 .always function test 36 .always class always function test 37 .always class always function test 38 . test_scope2.py always module always class always function test 39 .always function test 40 . ====== 7 passed in 0.01s ======

function は都度、 class はクラスごとに1度、 module はファイルごとに1度。 session はテスト呼び出しにつき1度呼ばれていることがわかりますね。

さて、 test_scope1.py にのみ always_for_scope1 というフィクスチャを module スコープで定義していますが、 test_scope2.py のテストケースで実行されていないことに気づいたでしょうか。 フィクスチャは書いたファイルでのみ実行されるのです。

では複数のファイルを横断したフィクスチャはどのようにするか。 これを実現するのが conftest.py です。 conftest.py に書いたフィクスチャはそのファイルが所属するディレクトリ配下のファイル全てが対象となります。(子ディレクトリも)

マーカー

pytestではユニットテストにマーカーを設定することで、テスト呼び出しを制御できます。

定義済みのマーカーは --markers オプションで確認できます。 以下のマーカーがデフォルトで存在します。(v6.2.5だと)

$ py.test --markers @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. @pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://docs.pytest.org/en/stable/reference.html#pytest-mark-skipif @pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/reference.html#pytest-mark-xfail @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/parametrize.html for more info and examples. @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/fixture.html#usefixtures @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
info
  • プラグインとして追加されるものもあります。
  • たとえば Django のユニットテストで使う pytest.mark.django_db はよく使います。

この記事ではよく使うものに絞って紹介していきます。

使い方

マーカー紹介の前にまず使い方から。

マーカーはデコレータなので、基本的には対象のクラスや関数(メソッド)をデコレートする形で適用します。 雑に公式ドキュメントの例を持ってきます。

  • クラスに適用する
  • 関数に適用する
  • import pytest @pytest.mark.webtest class TestClass: def test_startup(self): pass def test_startup_and_more(self): pass
  • @pytest.mark.foo @pytest.mark.parametrize( ("n", "expected"), [(1, 2), pytest.param(1, 3, marks=pytest.mark.bar), (2, 3)] ) def test_increment(n, expected): assert n + 1 == expected

あまり特筆することでもありませんが、デコレータを連ねて書くことで複数適用できます。

info
  • テストケースが多い場合、全てに書くのは大変です。
  • pytestmark という変数をグローバル領域に書くことでそのモジュールに属する全てのテストケースに適用されます。
    import pytest pytestmark = pytest.mark.webtest
  • 複数のときはリストで
    import pytest pytestmark = [pytest.mark.webtest, pytest.mark.slowtest]
  • 当記事ではこの書き方はせず、明示的にデコレータで適用していきます。

Working with custom markers — pytest documentationhttps://docs.pytest.org/en/6.2.x/example/markers.html

skip, skipif

skipマーカーが設定されたテストケースはテスト実行がスキップされます。 skipifはお察しの通り、条件を満たしたときのみスキップされるというものです。

スキップする理由はreason引数に指定します。 メモ的なもので動作には影響しませんができれば書いておいたほうが良いです。

以下(test_skipif.py)のように書くと

  • Pythonのバージョンが3.5以上の場合はtest_9がスキップ され、
  • 未満の場合はtest_10がスキップ されます。
  • py.test test_skip.py
  • py.test test_skipif.py
  • test_skip.py s [100%] ===== 1 skipped in 0.00s
  • test_skipif.py sF ===== FAILURES ====== _____ test_10 _______ @pytest.mark.skipif('sys.version_info < (3, 5)', reason="3.5以上だから") def test_10(): > assert False E assert False test_skipif.py:9: AssertionError ===== 1 failed, 1 skipped in 0.05s ======

条件には sys, os モジュールが使えるらしいです。(後述するxfailも同様)

xfail

テストが失敗することを期待するオプションです。

このオプションが指定されている状態で、失敗した場合は「x」、成功した場合は「X」で表示されます。

ここでいう失敗には例外も含まれますが、raises引数で例外を指定すると、その例外だけを失敗として期待します。

-rx オプションを付けることで xfailマーカー がついたテストケースの結果を表示できるのでここでは指定しましょう。

  • test files
  • py.test -rx test_xfail.py
  • test_xfail.py xXxFxxxFx [100%] =============================== FAILURES =============================== _______________________________ test_16 ________________________________ @pytest.mark.xfail('False', reason='condition=false') def test_16(): > assert False E assert False test_xfail.py:17: AssertionError _______________________________ test_20 ________________________________ @pytest.mark.xfail(raises=IndexError, reason='raises=IndexError') def test_20(): > assert False E assert False test_xfail.py:33: AssertionError ======================= short test summary info ======================== XFAIL test_xfail.py::test_13 XFAIL test_xfail.py::test_15 なんとなく XFAIL test_xfail.py::test_17 condition=true XFAIL test_xfail.py::test_18 reason: [NOTRUN] run=false XFAIL test_xfail.py::test_19 run=true XFAIL test_xfail.py::test_21 raises=IndexError =============== 2 failed, 6 xfailed, 1 xpassed in 0.11s ================

reason引数については先程と同様です。 run引数がFalseの場合はテストケースが実行されません。

parametrize

複数のパラメーターを使ったテストを簡潔に記述する方法です。 Golangのテーブルドリブンテストと似たようなものです。

少しだけ難しいかもしれませんがこれが使えるようになるとコードの冗長さをへらすことができ、テストを書くのが楽になるのでぜひ使えるようになりましょう!

繰り返し読み込ませたいパラメータ名とパラメータを引数に指定すると、そのパラメータの数だけテストケースを実行します。

  • test files
  • py.test test_parametrize.py
  • test_parametrize.py ..F [100%] =============================== FAILURES =============================== ____________________________ test_23[3-3-5] ____________________________ a = 3, b = 3, c = 5 @pytest.mark.parametrize(['a', 'b', 'c'], [ (1, 1, 1), (2, 2, 2), (3, 3, 5), ]) def test_23(a, b, c): > assert a == b == c E assert 3 == 5 test_parametrize.py:9: AssertionError ------------------------ Captured stdout setup ------------------------- always class always function ======================= short test summary info ======================== FAILED test_parametrize.py::test_23[3-3-5] - assert 3 == 5 ===================== 1 failed, 2 passed in 0.08s ======================

第一引数に指定したものがパラメータ名となり、テストケースの実引数として受け取ることになるのでこれを使ってテストをするというわけです。

上記のコードでは、以下の条件で3回繰り返されますが、3回目では assert a == b == c を満たさずテストがFailします。

変数
    • a
    • b
    • c
1回目(Pass)
    • 1
    • 1
    • 1
2回目(Pass)
    • 2
    • 2
    • 2
3回目(Fail)
    • 3
    • 3
    • 5

usefixtures

フィクスチャは引数に指定したりautouseを使うことで適用すると書きましたが、 usefixtures マーカーを使う方法もあります。

# content of test_setenv.py import os import pytest @pytest.mark.usefixtures("cleandir") class TestDirectoryInit: def test_cwd_starts_empty(self): assert os.listdir(os.getcwd()) == [] with open("myfile", "w") as f: f.write("hello") def test_cwd_again_starts_empty(self): assert os.listdir(os.getcwd()) == []

正直なことを言えば、今までやってきたプロジェクトで usefixtures マーカーを使っていないので なくてもなんとかなるものではありますが、以下のようなメリットがあります。

  • テストケースの中で直接参照しないフィクスチャを引数に指定しなくてよいため unused の警告がでない

    • autouse と違い特定のテストケースにだけ適用できる
  • 複数のフィクスチャを別のマーカーとして切り出して別の箇所で利用できる

    pytestmark = pytest.mark.usefixtures("something1", "something2")
  • pytest.ini に usefixtures を指定することでプロジェクト全体にフィクスチャを適用できる

    [pytest] usefixtures = cleandir

pytest fixtures: explicit, modular, scalable — pytest documentationhttps://docs.pytest.org/en/6.2.x/fixture.html#use-fixtures-in-classes-and-modules-with-usefixtures

info
  • フィクスチャは名前により同一プロセス内で一意に識別されるため、テストケースからオブジェクトとして参照できる必要はありません。 「usefixtures」の実引数を文字列指定するのはこのためでしょう。
  • 逆にテストケースメソッドでは引数名によりフィクスチャを特定します。(メソッド定義では仮引数に文字列指定できないため)

カスタムマーカー

今までは動作に影響のあるマーカーばかりでしたが、 自分で定義したマーカーは基本的に動作を持たないため前述したように単にラベルやタグのような働きしかしません。

マーカーに意味を持たせるためにはfixtureかフックからマーカーを参照して何らかの動作をするように実装する必要がありそうです。 今回は扱いません。

上記のように動作が定義されていないマーカーにも使い道はあります。 以下のテストケースを test_custom_markers.py として設置します。

-m にオプションを指定して実行してみましょう。

  • -m "a"
  • -m "a and b"
  • -m "a and not b"
  • $ py.test test_custom_markers.py -m "a" --collect-only collected 3 items <Module test_custom_markers.py> <Function test_41> <Function test_42> <Class TestMine> <Function test_43> ====================== 3 tests collected in 0.02s ======================
  • $ py.test test_custom_markers.py -m "a and b" --collect-only collected 3 items / 1 deselected / 2 selected <Module test_custom_markers.py> <Function test_41> <Class TestMine> <Function test_43> ============= 2/3 tests collected (1 deselected) in 0.02s ==============
  • $ py.test test_custom_markers.py -m "a and not b" --collect-only collected 3 items / 2 deselected / 1 selected <Module test_custom_markers.py> <Function test_42> ============= 1/3 tests collected (2 deselected) in 0.02s ==============

「-m」オプションを使うことで、「このマーカーのついているマーカー」、且つ、「このマーカーがついてないやつ」みたいな実行制御ができます。

マーカーは完全一致なので、「a」マーカーを指定してるのに「aa」マーカーのテストケースが取れるということはありません。

なんか、タグ検索みたいですよね?ね?

番外編

request

「request」という特殊なフィクスチャが存在します。

テストケースの中でrequestフィクスチャを参照すると テストケースの外側のようなメタ情報を取得したり終了時に実行するファイナライザを定義することができます。

以下のように使います。

@pytest.mark.test1 @pytest.mark.test2(1, b=2) def test_req(request): print('req', dict(request.keywords)) def exit(): print('exit') request.addfinalizer(exit)

マーカー定義も取れていて、ファイナライザも実行されてるみたいですね。

py.test test_fixture.py -s req {'test2': True, 'test1': True, 'tests': True, 'pytestmark': [Mark(name='test2', args=(1,), kwargs={'b': 2}), Mark(name='test1', args=(), kwargs={})], 'test_request.py': True, 'test_req': True} .exit ==================== 1 passed, 2 warnings in 0.01s =====================

これで大まかな使い方はわかったような気がする。

次はtoxかpytestのプラギンについて書くような気がします。