pytestを使用したPythonテスト。 ビルトインフィクスチャ、第4章

戻る 次へ







pytestに付属している組み込みのフィクスチャを使用すると、テストで非常に便利なことを簡単かつ自然に行うことができます。 たとえば、一時ファイルの処理に加えて、pytestには、コマンドラインパラメーターへのアクセス、テストセッション間の通信、出力ストリームの確認、環境変数の変更、アラートのポーリングのための組み込みフィクスチャが含まれています。













Tasksプロジェクトのソースコードと、この本に示されているすべてのテストのソースコードは、 pragprog.comにある本のWebページのリンクから入手できます。 テストコードを理解するためにソースコードをダウンロードする必要はありません。 テストコードは、例では便利な形式で示されています。 ただし、プロジェクトのタスクを実行したり、テストサンプルを調整して自分のプロジェクトをチェックしたりするには(手を縛っていない!)、書籍のWebページにアクセスして作品をダウンロードする必要があります。 書籍のウェブページには、 正誤表の投稿とディスカッションフォーラムへのリンクがあります。

ネタバレの下には、このシリーズの記事のリストがあります。









前の章では、フィクスチャとは何か、フィクスチャの書き方、テストデータとセットアップおよび分解コードのためにそれらを使用する方法について見てきました。







また、conftest.pyを使用して、複数のテストファイルのテスト間でフィクスチャを共有しました。 第3章の終わりに、Tasksプロジェクトの49ページのpytest Fixturesに次のフィクスチャがインストールされました。それらが必要です。







通常のフィクスチャを再利用することは、pytestの開発者が一般的に必要とするフィクスチャをpytestに含めたという良いアイデアです。 59ページのTasksプロジェクトのフィクスチャのスコープ変更セクションで、Tasksプロジェクトがtmpdirとtmpdir_factoryをどのように使用するかを既に見てきました。これらについては、この章で詳しく説明します。







pytestに付属している組み込みのフィクスチャは、テストで非常に便利なことを簡単かつ自然に行うのに役立ちます。 たとえば、一時ファイルの処理に加えて、pytestには、コマンドラインパラメーターへのアクセス、テストセッション間の通信、出力ストリームのチェック、環境変数の変更、およびアラートのポーリングのための組み込みフィクスチャが含まれています。 組み込みのフィクスチャは、pytestコア機能の拡張機能です。 次に、最も一般的に使用されるいくつかのインラインフィクスチャを順番に見てみましょう。







tmpdirおよびtmpdir_factoryの使用



ファイルの読み取り、書き込み、または変更を行うものをテストする場合は、tmpdirを使用して単一のテストで使用されるファイルまたはディレクトリを作成できます。また、複数のテスト用にディレクトリを設定する場合はtmpdir_factory



を使用できます。







tmpdir tmpdir



には関数スコープがあり、 tmpdir_factory



フィクスチャにはセッションスコープがあります。 1つのテストだけに一時ディレクトリまたはファイルを必要とする単一のテストでは、 tmpdir



を使用できます。 これはフィクスチャにも当てはまります。フィクスチャは、テスト機能ごとに再作成する必要があるディレクトリまたはファイルをカスタマイズします。







tmpdir



を使用した簡単な例を次に示しtmpdir









ch4/test_tmpdir.py





 def test_tmpdir(tmpdir): # tmpdir    ,    # join()  ,    , #     a_file = tmpdir.join('something.txt') #    a_sub_dir = tmpdir.mkdir('anything') #      (  ) another_file = a_sub_dir.join('something_else.txt') #    'something.txt' a_file.write('contents may settle during shipping') #    'anything/something_else.txt' another_file.write('something different') #      assert a_file.read() == 'contents may settle during shipping' assert another_file.read() == 'something different'
      
      





tmpdir



から返される値は、タイプpy.path.local.1



オブジェクトです。これは、一時ディレクトリとファイルに必要なもののすべてです。 ただし、1つのトリックがあります。 tmpdir



フィクスチャtmpdir



関数スコープとして定義されているため、 tmpdir



使用して、複数のテスト関数で使用できるフォルダーまたはファイルを作成することはできません。 関数(クラス、モジュール、セッション)以外のスコープを持つフィクスチャの場合、 tmpdir_factory



が利用可能です。







tmpdir_factory tmpdir_factory



tmpdir



に非常に似ていますが、異なるインターフェースを持っています。 「スコープフィクスチャの仕様」セクション(56ページ)で説明されているように、関数エリアフィクスチャは各テスト関数に対して1回実行され、モジュールエリアフィクスチャはモジュールごとに1回実行され、クラスフィクスチャはクラスごとに1回実行され、エリア検証テストセッションごとに1回動作します。 したがって、セッション領域レコードで作成されたリソースには、セッション全体の有効期間があります。 tmpdir



tmpdir_factory



類似性を示すために、 tmpdir_factory



tmpdir



例をtmpdir









ch4 / test_tmpdir.py


 def test_tmpdir_factory(tmpdir_factory): #      . a_dir   # ,    tmpdir a_dir = tmpdir_factory.mktemp('mydir') # base_temp    'mydir'    #  getbasetemp(),  # ,    base_temp = tmpdir_factory.getbasetemp() print('base:', base_temp) #       , #    ' test_tmpdir ()',   , #    a_dir  tmpdir a_file = a_dir.join('something.txt') a_sub_dir = a_dir.mkdir('anything') another_file = a_sub_dir.join('something_else.txt') a_file.write('contents may settle during shipping') another_file.write('something different') assert a_file.read() == 'contents may settle during shipping' assert another_file.read() == 'something different'
      
      





最初の行はmktemp('mydir')



を使用してディレクトリを作成し、 a_dir



ます。 関数の残りの部分では、 tmpdir



から返されるtmpdir



と同じ方法でa_dir



を使用できます。







tmpdir_factory



例の2行目では、 getbasetemp()



関数はこのセッションに使用されるベースディレクトリを返します。 この例のprintステートメントは、システム上のディレクトリを表示できるようにするために必要です。 それがどこにあるか見てみましょう:







 $ cd /path/to/code/ch4 $ pytest -q -s test_tmpdir.py::test_tmpdir_factory base: /private/var/folders/53/zv4j_zc506x2xq25l31qxvxm0000gn/T/pytest-of-okken/pytest-732 . 1 passed in 0.04 seconds
      
      





このベースディレクトリはシステムとユーザーに依存し、 pytest - NUM



が増加するたびにpytest - NUM



がセッションごとに変化します。 ベースディレクトリは、セッション後にそのまま残されます。 pytestはそれをクリーンアップし、最後の数個の一時ベースディレクトリのみがシステムに残ります。これは、テスト実行後にファイルをチェックするのが待ち遠しい場合は問題ありません。







pytest --basetemp=mydir



を使用する必要がある場合は、独自のベースディレクトリを指定することもできます。







他の領域に一時ディレクトリを使用する



tmpdir_factory



から一時ディレクトリとセッション領域ファイルを取得し、 tmpdir



から関数ディレクトリと領域ファイルを取得します。 しかし、他の領域はどうですか? モジュールまたはクラスのスコープの一時ディレクトリが必要な場合はどうなりますか? これを行うには、目的のサイズの領域の別のフィクスチャを作成します。これにはtmpdir_factory



を使用する必要があります。







たとえば、テストでいっぱいのモジュールがあり、それらの多くがjsonファイルからいくつかのデータを読み取ることができると仮定します。 フィクスチャボリュームフィクスチャをモジュール自体またはconftest.pyファイルに配置することができました。これにより、データファイルが次のように設定されます。







ch4 / authors / conftest.py


 """Demonstrate tmpdir_factory.""" import json import pytest @pytest.fixture(scope='module') def author_file_json(tmpdir_factory): """     .""" python_author_data = { 'Ned': {'City': 'Boston'}, 'Brian': {'City': 'Portland'}, 'Luciano': {'City': 'Sau Paulo'} } file = tmpdir_factory.mktemp('data').join('author_file.json') print('file:{}'.format(str(file))) with file.open('w') as f: json.dump(python_author_data, f) return file
      
      





フィクスチャauthor_file_json()



は、 dataという一時ディレクトリを作成し、データディレクトリにauthor_file.jsonというファイルを作成します。 次に、 python_author_data



辞書をjsonとして書き込みます。 これはフィクスチャエリアモジュールであるため、テストを使用してモジュールごとにjsonファイルが1回だけ作成されます。







ch4 / authors / test_authors.py


 """ ,    .""" import json def test_brian_in_portland(author_file_json): """,   .""" with author_file_json.open() as f: authors = json.load(f) assert authors['Brian']['City'] == 'Portland' def test_all_have_cities(author_file_json): """        .""" with author_file_json.open() as f: authors = json.load(f) for a in authors: assert len(authors[a]['City']) > 0
      
      





両方のテストで同じJSONファイルが使用されます。 1つのテストデータファイルが複数のテストで機能する場合、両方のテストで再作成しても意味がありません。







pytestconfigを使用する



pytestconfigビルトインフィクスチャを使用すると、pytestが引数とコマンドラインオプション、構成ファイル、プラグイン、およびpytestを起動したディレクトリでどのように動作するかを制御できます。 pytestconfigフィクスチャはrequest.configのショートカットであり、pytestのドキュメントでは「 pytest configオブジェクト 」(pytest構成オブジェクト)と呼ばれることもあります。







pytestconfigの仕組みを調べるには、カスタムコマンドラインパラメーターを追加し、テストからパラメーター値を読み取る方法を確認できます。 pytestconfigからコマンドラインパラメーターの値を直接読み取ることができますが、パラメーターを追加して分析するには、フック関数を追加する必要があります。 95ページの第5章「プラグイン」で詳しく説明するフック関数は、pytestの動作を制御する別の方法であり、プラグインでよく使用されます。 ただし、カスタムコマンドラインオプションを追加してpytestconfigから読み取ること非常に一般的であるため、ここで説明します。







pytestフック pytest_addoption



を使用して、pytestコマンドラインで既に利用可能なパラメーターにいくつかのパラメーターを追加します。







ch4 / pytestconfig / conftest.py


 def pytest_addoption(parser): parser.addoption("--myopt", action="store_true", help="some boolean option") parser.addoption("--foo", action="store", default="bar", help="foo: bar or baz")
      
      





pytest_addoption



を使用してコマンドラインパラメーターを追加するには、プラグインを使用するか、プロジェクトディレクトリ構造の最上部にあるconftest.pyファイルを使用します。 testサブディレクトリでこれを行うべきではありません。







--myopt



および--foo <value>



オプションが前のコードに追加され、ヘルプ行が次のように変更されました。







 $ cd /path/to/code/ch4/pytestconfig $ pytest --help usage: pytest [options] [file_or_dir] [file_or_dir] [...] ... custom options: --myopt some boolean option --foo=FOO foo: bar or baz ...
      
      





これで、テストからこれらのオプションにアクセスできます。







ch4 / pytestconfig / test_config.py


 import pytest def test_option(pytestconfig): print('"foo" set to:', pytestconfig.getoption('foo')) print('"myopt" set to:', pytestconfig.getoption('myopt'))
      
      





仕組みを見てみましょう。







 $ pytest -s -q test_config.py::test_option "foo" set to: bar "myopt" set to: False .1 passed in 0.01 seconds $ pytest -s -q --myopt test_config.py::test_option "foo" set to: bar "myopt" set to: True .1 passed in 0.01 seconds $ pytest -s -q --myopt --foo baz test_config.py::test_option "foo" set to: baz "myopt" set to: True .1 passed in 0.01 seconds
      
      





pytestconfigはフィクスチャであるため、他のフィクスチャからも取得できます。 必要に応じて、オプション名のフィクスチャを作成できます。例:







ch4 / pytestconfig / test_config.py


 @pytest.fixture() def foo(pytestconfig): return pytestconfig.option.foo @pytest.fixture() def myopt(pytestconfig): return pytestconfig.option.myopt def test_fixtures_for_options(foo, myopt): print('"foo" set to:', foo) print('"myopt" set to:', myopt)
      
      





また、追加されたパラメーターだけでなく、組み込みパラメーターにアクセスしたり、pytestの起動方法に関する情報(ディレクトリ、引数など)にアクセスしたりすることもできます。







次に、いくつかの値と構成オプションの例を示します。







 def test_pytestconfig(pytestconfig): print('args :', pytestconfig.args) print('inifile :', pytestconfig.inifile) print('invocation_dir :', pytestconfig.invocation_dir) print('rootdir :', pytestconfig.rootdir) print('-k EXPRESSION :', pytestconfig.getoption('keyword')) print('-v, --verbose :', pytestconfig.getoption('verbose')) print('-q, --quiet :', pytestconfig.getoption('quiet')) print('-l, --showlocals:', pytestconfig.getoption('showlocals')) print('--tb=style :', pytestconfig.getoption('tbstyle'))
      
      





113ページの第6章「構成」でiniファイルをデモンストレーションするときに、pytestconfigに戻ります。







キャッシュを使用する



通常、テスターは、各テストが他のテストから可能な限り独立していると考えています。 注文会計の依存関係が入り込まないことを確認する必要があります。 任意のテストを任意の順序で実行または再起動して、同じ結果が得られるようにしたいと思います。 さらに、テストセッションは繰り返し可能で、以前のテストセッションに基づいて動作を変更しない必要があります。







ただし、あるテストセッションから別のテストセッションに情報を転送することが非常に役立つ場合があります。 将来のテストセッションに情報を渡したい場合は、組み込みのcache



フィクスチャを使用してこれを行うことができます。







フィクスチャcache



、1つのテストセッションに関する情報を保存し、次のセッションで取得するように設計されています。 このケースの利益のためにcache



パーミッションを使用する素晴らしい例は、組み込み機能--last-failed



および--failed-first



です。 これらのフラグのデータがキャッシュにどのように保存されるかを見てみましょう。







--last-failed



および--failed-first



オプションのヘルプテキストと、いくつかのcache



オプションを--failed-first



ます。







 $ pytest --help ... --lf, --last-failed rerun only the tests that failed at the last run (or all if none failed) --ff, --failed-first run all tests but run the last failures first. This may re-order tests and thus lead to repeated fixture setup/teardown --cache-show show cache contents, don t perform collection or tests --cache-clear remove all cache contents at start of test run. ...
      
      





それらの動作を確認するには、次の2つのテストを使用します。







ch4 /キャッシュ/ test_pass_fail.py







 def test_this_passes(): assert 1 == 1 def test_this_fails(): assert 1 == 2
      
      





関数名を表示するには--verbose



を使用して実行し、スタックトレースを非表示にするには--tb=no



を使用します。







 $ cd /path/to/code/ch4/cache $ pytest --verbose --tb=no test_pass_fail.py ==================== test session starts ==================== collected 2 items test_pass_fail.py::test_this_passes PASSED test_pass_fail.py::test_this_fails FAILED ============ 1 failed, 1 passed in 0.05 seconds =============
      
      





--ff



または--failed-first



フラグを--ff



して再度実行すると、以前に失敗したテストが最初に実行され、次にセッション全体が実行されます。







 $ pytest --verbose --tb=no --ff test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures first collected 2 items test_pass_fail.py::test_this_fails FAILED test_pass_fail.py::test_this_passes PASSED ============ 1 failed, 1 passed in 0.04 seconds =============
      
      





または、 --lf



または--last-failed



を使用して、前回失敗したテストのみを実行できます。







 $ pytest --verbose --tb=no --lf test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures collected 2 items test_pass_fail.py::test_this_fails FAILED ==================== 1 tests deselected ===================== ========== 1 failed, 1 deselected in 0.05 seconds ===========
      
      





クラッシュデータの保存方法と同じメカニズムの使用方法を説明する前に、 --lf



および--ff



値をさらに明確にする別の例を見てみましょう。







以下は、1つの失敗を伴うパラメーター化されたテストです。







ch4 /キャッシュ/ test_few_failures.py


 """Demonstrate -lf and -ff with failing tests.""" import pytest from pytest import approx testdata = [ # x, y, expected (1.01, 2.01, 3.02), (1e25, 1e23, 1.1e25), (1.23, 3.21, 4.44), (0.1, 0.2, 0.3), (1e25, 1e24, 1.1e25) ] @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y assert sum_ == approx(expected)
      
      





そして出力では:







 $ cd /path/to/code/ch4/cache $ pytest -q test_few_failures.py .F... ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) test_few_failures.py:17: AssertionError 1 failed, 4 passed in 0.06 seconds
      
      





すぐに問題を特定できるかもしれません。 しかし、テストがより長く、より複雑であり、ここで何が間違っているかはそれほど明白ではないと想像してみましょう。 もう一度テストを実行して、エラーを確認します。 テストケースはコマンドラインで指定できます。







 $ pytest -q "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]"
      
      





コピー/貼り付け(コピー/貼り付け )をしたくない場合、または再起動したい不幸なケースがいくつかある場合--lf



--lf



はるかに簡単です。 実際にテストの失敗をデバッグしている場合、状況を緩和する可能性のある別のフラグは--showlocals



、または略して-l



です。







 $ pytest -q --lf -l test_few_failures.py F ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) expected = 1.1e+25 sum_ = 1.01e+25 x = 1e+25 y = 1e+23 test_few_failures.py:17: AssertionError ================= 4 tests deselected ================= 1 failed, 4 deselected in 0.05 seconds
      
      





失敗の理由はより明白であるはずです。







前回テストが失敗したことを覚えておくために、ちょっとしたトリックがあります。 pytestは最後のテストセッションからのテストエラー情報を保存し、-- --cache-show



保存された情報を表示できます。







 $ pytest --cache-show ===================== test session starts ====================== ------------------------- cache values ------------------------- cache/lastfailed contains: {'test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]': True} ================= no tests ran in 0.00 seconds =================
      
      





または、キャッシュディレクトリを確認できます。







 $ cat .cache/v/cache/lastfailed { "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]": true }
      
      





--clear-cache



スイッチを使用すると、セッションの前にキャッシュをクリアできます。







キャッシュは--lf



および--ff



だけでなく使用できます。 テストにかかった時間を記録し、時間を節約し、テストでエラーを報告する次回の記録を作成するフィクスチャを作成しましょう。前回のテストの2倍の時間がかかります。







キャッシュフィクスチャのインターフェイスはシンプルです。







 cache.get(key, default) cache.set(key, value)
      
      





慣例により、キー名はアプリケーションまたはプラグインの名前で始まり、その後に/



が続き、キー名セクションは/



区切られます。 格納する値は、 .cache directory



表されるため、 jsonに変換される任意のものにすることができます。







テスト時間を修正するために使用されるフィクスチャは次のとおりです。







ch4 /キャッシュ/ test_slower.py







 @pytest.fixture(autouse=True) def check_duration(request, cache): key = 'duration/' + request.node.nodeid.replace(':', '_') #   (nodeid)    #      .cache #    -     start_time = datetime.datetime.now() yield stop_time = datetime.datetime.now() this_duration = (stop_time - start_time).total_seconds() last_duration = cache.get(key, None) cache.set(key, this_duration) if last_duration is not None: errorstring = "       2-  " assert this_duration <= last_duration * 2, errorstring
      
      





フィクスチャはautouseであるため、テストから参照する必要はありません。 要求オブジェクトは、キーで使用するノードIDを取得するために使用されます。 nodeidは、パラメーター化されたテストでも機能する一意の識別子です。 キャッシュの適切な居住者になるように、「duration /」というキーを追加します。 上記のコードは、テスト関数の前に実行されます。 yieldの後のコードは、テスト関数の後に実行されます。







ここで、異なる時間間隔をとるいくつかのテストが必要です。







ch4 /キャッシュ/ test_slower.py







 @pytest.mark.parametrize('i', range(5)) def test_slow_stuff(i): time.sleep(random.random())
      
      





おそらくこのための一連のテストを書きたくないので、 random



とパラメーター化を使用して、1秒よりも短いランダムな時間スリープするテストを簡単に生成しました。 これがどのように機能するかを数回見てみましょう。







 $ cd /path/to/code/ch4/cache $ pytest -q --cache-clear test_slower.py ..... 5 passed in 2.10 seconds $ pytest -q --tb=line test_slower.py ...E..E =================================== ERRORS ==================================== ___________________ ERROR at teardown of test_slow_stuff[1] ___________________ E AssertionError: test duration over 2x last duration assert 0.35702 <= (0.148009 * 2) ___________________ ERROR at teardown of test_slow_stuff[4] ___________________ E AssertionError: test duration over 2x last duration assert 0.888051 <= (0.324019 * 2) 5 passed, 2 error in 3.17 seconds
      
      





まあ、それは楽しかったです。 キャッシュの内容を見てみましょう。







 $ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower.py::test_slow_stuff[2]': True, 'test_slower.py::test_slow_stuff[4]': True} cache\nodeids contains: ['test_slower.py::test_slow_stuff[0]', 'test_slower.py::test_slow_stuff[1]', 'test_slower.py::test_slow_stuff[2]', 'test_slower.py::test_slow_stuff[3]', 'test_slower.py::test_slow_stuff[4]'] cache\stepwise contains: [] duration\test_slower.py__test_slow_stuff[0] contains: 0.958055 duration\test_slower.py__test_slow_stuff[1] contains: 0.214012 duration\test_slower.py__test_slow_stuff[2] contains: 0.19001 duration\test_slower.py__test_slow_stuff[3] contains: 0.725041 duration\test_slower.py__test_slow_stuff[4] contains: 0.836048 no tests ran in 0.03 seconds
      
      





キャッシュデータ名のプレフィックスにより、 duration



データをキャッシュデータとは別に簡単に表示できます。 ただし、 lastfailed



機能が単一のキャッシュエントリで機能することは興味深いことlastfailed



。 期間データは、テストごとに1つのキャッシュエントリを占有します。 lastfailedの例に従って、データを1つのレコードに入れましょう。







テストごとに読み取りとキャッシュを行います。 フィクスチャを関数のスコープのフィクスチャに分割して、キャッシュの読み取りと書き込みを行うセッションのスコープの期間とフィクスチャを測定できます。 ただし、これを行うと、関数のスコープがあるためキャッシュフィクスチャを使用できません。幸いなことに、GitHubの実装をざっと見てみると、キャッシュフィクスチャが単にを返しいることがわかりますrequest.config.cache



それはあらゆる地域で利用可能です。







同じ機能の可能な再編成の1つを次に示します。







ch4 /キャッシュ/ test_slower_2.py







 Duration = namedtuple('Duration', ['current', 'last']) @pytest.fixture(scope='session') def duration_cache(request): key = 'duration/testdurations' d = Duration({}, request.config.cache.get(key, {})) yield d request.config.cache.set(key, d.current) @pytest.fixture(autouse=True) def check_duration(request, duration_cache): d = duration_cache nodeid = request.node.nodeid start_time = datetime.datetime.now() yield duration = (datetime.datetime.now() - start_time).total_seconds() d.current[nodeid] = duration if d.last.get(nodeid, None) is not None: errorstring = "test duration over 2x last duration" assert duration <= (d.last[nodeid] * 2), errorstring
      
      





duration_cache



. , , - . , namedtuple



Duration current



last



. namedtuple



test_duration



, . , namedtuple



, d.current



. .







, :







 $ pytest -q --cache-clear test_slower_2.py ..... 5 passed in 2.80 seconds $ pytest -q --tb=no test_slower_2.py ...EE.. 7 passed, 2 error in 3.21 seconds $ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower_2.py::test_slow_stuff[2]': True, 'test_slower_2.py::test_slow_stuff[3]': True} duration\testdurations contains: {'test_slower_2.py::test_slow_stuff[0]': 0.483028, 'test_slower_2.py::test_slow_stuff[1]': 0.198011, 'test_slower_2.py::test_slow_stuff[2]': 0.426024, 'test_slower_2.py::test_slow_stuff[3]': 0.762044, 'test_slower_2.py::test_slow_stuff[4]': 0.056003, 'test_slower_2.py::test_slow_stuff[5]': 0.18401, 'test_slower_2.py::test_slow_stuff[6]': 0.943054} no tests ran in 0.02 seconds
      
      





.







capsys



capsys builtin



: stdout stderr , . stdout stderr.







, stdout:







ch4/cap/test_capsys.py


 def greeting(name): print('Hi, {}'.format(name))
      
      





, . - stdout. capsys:







ch4/cap/test_capsys.py


 def test_greeting(capsys): greeting('Earthling') out, err = capsys.readouterr() assert out == 'Hi, Earthling\n' assert err == '' greeting('Brian') greeting('Nerd') out, err = capsys.readouterr() assert out == 'Hi, Brian\nHi, Nerd\n' assert err == ''
      
      





stdout



stderr



capsys.redouterr()



. — , , .







stdout



. , stderr



:







 def yikes(problem): print('YIKES! {}'.format(problem), file=sys.stderr) def test_yikes(capsys): yikes('Out of coffee!') out, err = capsys.readouterr() assert out == '' assert 'Out of coffee!' in err
      
      





pytest . print . . -s



, stdout . , , . , pytest , , . capsys . capsys.disabled()



, .







以下に例を示します。







ch4/cap/test_capsys.py


 def test_capsys_disabled(capsys): with capsys.disabled(): print('\nalways print this') #    print('normal print, usually captured') #  ,  
      
      





, 'always print this' :







 $ cd /path/to/code/ch4/cap $ pytest -q test_capsys.py::test_capsys_disabled
      
      





, always print this



, capys.disabled()



. print — print, normal print, usually captured



( , ), , -s



, --capture=no



, .







monkeypatch



"monkey patch" — . "monkey patching" — , , . monkeypatch



. , , , , . , . API , monkeypatch .







monkeypatch



:









raising



pytest, , . prepend



setenv()



. , + prepend + <old value>



.







monkeypatch , , dot- . , dot- . , cheese- :







ch4/monkey/cheese.py


 import os import json def read_cheese_preferences(): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'r') as f: prefs = json.load(f) return prefs def write_cheese_preferences(prefs): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'w') as f: json.dump(prefs, f, indent=4) def write_default_cheese_preferences(): write_cheese_preferences(_default_prefs) _default_prefs = { 'slicing': ['manchego', 'sharp cheddar'], 'spreadable': ['Saint Andre', 'camembert', 'bucheron', 'goat', 'humbolt fog', 'cambozola'], 'salads': ['crumbled feta'] }
      
      





, write_default_cheese_preferences()



. , . , . .







, . , read_cheese_preferences()



, , write_default_cheese_preferences()



:







ch4/monkey/test_cheese.py


 def test_def_prefs_full(): cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
      
      





, , , cheese- . .







HOME set



, os.path.expanduser()



~



, HOME



. HOME



, :







ch4/monkey/test_cheese.py


 def test_def_prefs_change_home(tmpdir, monkeypatch): monkeypatch.setenv('HOME', tmpdir.mkdir('home')) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
      
      





, HOME



. - expanduser()



, , «On Windows, HOME and USERPROFILE will be used if set, otherwise a combination of….» . わあ! , Windows. , .







, HOME



, expanduser



:







ch4/monkey/test_cheese.py


 def test_def_prefs_change_expanduser(tmpdir, monkeypatch): fake_home_dir = tmpdir.mkdir('home') monkeypatch.setattr(cheese.os.path, 'expanduser', (lambda x: x.replace('~', str(fake_home_dir)))) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
      
      





, cheese os.path.expanduser()



-. re.sub



~



. setenv()



setattr()



. , setitem()



.







, , , . , , write_default_cheese_preferences()



:







ch4/monkey/test_cheese.py


 def test_def_prefs_change_defaults(tmpdir, monkeypatch): #      fake_home_dir = tmpdir.mkdir('home') monkeypatch.setattr(cheese.os.path, 'expanduser', (lambda x: x.replace('~', str(fake_home_dir)))) cheese.write_default_cheese_preferences() defaults_before = copy.deepcopy(cheese._default_prefs) #     monkeypatch.setitem(cheese._default_prefs, 'slicing', ['provolone']) monkeypatch.setitem(cheese._default_prefs, 'spreadable', ['brie']) monkeypatch.setitem(cheese._default_prefs, 'salads', ['pepper jack']) defaults_modified = cheese._default_prefs #       cheese.write_default_cheese_preferences() #    actual = cheese.read_cheese_preferences() assert defaults_modified == actual assert defaults_modified != defaults_before
      
      





_default_prefs



- , monkeypatch.setitem()



, .







setenv()



, setattr()



setitem()



. del



. , , - . monkeypatch



.







syspath_prepend(path)



sys.path



, , . stub-. monkeypatch.syspath_prepend()



, , stub-.







chdir(path)



. , . , , monkeypatch.chdir(the_tmpdir)



.







monkeypatch unittest.mock



, . 7 " pytest " . 125.







doctest_namespace



doctest Python docstrings , , . pytest doctest Python --doctest-modules



. doctest_namespace



, autouse , pytest doctest . docstrings .







doctest_namespace



, Python . , numpy



import numpy as np



.







. , unnecessary_math.py



multiply()



divide()



, . , docstring , docstrings :







ch4/dt/1/unnecessary_math.py


 """ This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> um.divide(10, 5) 2.0 """ return a / b
      
      





unnecessary_math



, um



, import noecessary_math as um



-. docstrings import



, um



. , pytest docstring . docstring , docstrings :







 $ cd /path/to/code/ch4/dt/1 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED unnecessary_math.py::unnecessary_math.divide FAILED unnecessary_math.py::unnecessary_math.multiply FAILED ================================== FAILURES =================================== ______________________ [doctest] unnecessary_math.divide ______________________ 034 035 Returns a divided by b. 036 037 >>> um.divide(10, 5) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.divide[0]>", line 1, in <module> NameError: name 'um' is not defined ... _____________________ [doctest] unnecessary_math.multiply _____________________ 022 023 Returns a multiplied by b. 024 025 >>> um.multiply(4, 3) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.multiply[0]>", line 1, in <module> NameError: name 'um' is not defined /path/to/code/ch4/dt/1/unnecessary_math.py:23: UnexpectedException ================ 2 failed, 1 passed in 0.03 seconds =================
      
      





- import



docstring:







ch4/dt/2/unnecessary_math.py


 """ This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> import unnecessary_math as um >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> import unnecessary_math as um >>> um.divide(10, 5) 2.0 """ return a / b
      
      





:







 $ cd /path/to/code/ch4/dt/2 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED [ 33%] unnecessary_math.py::unnecessary_math.divide PASSED [ 66%] unnecessary_math.py::unnecessary_math.multiply PASSED [100%] ===================== 3 passed in 0.03 seconds ======================
      
      





docstrings .







doctest_namespace



, autouse



conftest.py



, :







ch4/dt/3/conftest.py


 import pytest import unnecessary_math @pytest.fixture(autouse=True) def add_um(doctest_namespace): doctest_namespace['um'] = unnecessary_math
      
      





pytest um



doctest_namespace



, unnecessary_math



. conftest.py, doctests, conftest.py um



.







doctest pytest 7 " pytest " . 125.







recwarn



recwarn



, . Python , , , . , , , , . :







ch4/test_warnings.py


 import warnings import pytest def lame_function(): warnings.warn("Please stop using this", DeprecationWarning) # rest of function
      
      





, :







ch4/test_warnings.py


 def test_lame_function(recwarn): lame_function() assert len(recwarn) == 1 w = recwarn.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this'
      
      





recwarn , category



(), message



(), filename



( ) lineno



( ), .







. , , , . recwarn.clear()



, , .







recwarn



, pytest pytest.warns()



:







ch4/test_warnings.py


 def test_lame_function_2(): with pytest.warns(None) as warning_list: lame_function() assert len(warning_list) == 1 w = warning_list.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this'
      
      





pytest.warns()



. recwarn



pytest.warns()



, , , .









  1. ch4/cache/test_slower.py



    autouse



    , check_duration()



    . ch3/tasks_proj/tests/conftest.py



    .
  2. 第3章のテストを実行します。
  3. 非常に高速なテストの場合、2倍高速は依然として非常に高速です。2倍ではなく、フィクスチャを変更して、0.1秒と最後の継続時間の2倍を確認します。
  4. 変更されたフィクスチャでpytestを実行します。結果は合理的ですか?


次は何ですか



この章では、組み込みのpytestフィクスチャをいくつか見てきました。次に、プラグインについてさらに詳しく検討します。大きなプラグインを書くことのニュアンスは、それ自体が本になることがあります。ただし、小さなカスタムプラグインは、pytestエコシステムの一部です。







戻る 次へ








All Articles