pytestを使用したPythonテスト。 第2章、テスト関数の作成

戻る 次へ







テストをクラス、モジュール、ディレクトリに整理する方法を学びます。 次に、マーカーを使用して実行するテストをマークする方法を示し、組み込みマーカーがどのようにテストをスキップしてテストをマークし、失敗を期待できるかについて説明します。 最後に、テストのパラメーター化について説明します。これにより、異なるデータでテストを呼び出すことができます。













この本の例は、Python 3.6とpytest 3.2を使用して書かれています。 pytest 3.2は、Python 2.6、2.7、およびPython 3.3+をサポートしています。







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

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









前の章では、pytestを実行しました。 ファイルとディレクトリを使用して実行する方法と、機能するオプションの数を確認しました。 この章では、Pythonパッケージのテストのコンテキストでテスト関数を作成する方法を学習します。 pytestを使用してPythonパッケージ以外のものをテストする場合、この章のほとんどが役立ちます。







Tasksパッケージのテストを作成します。 これを行う前に、Python配布パッケージの構造とそのテスト、およびテストにテストパッケージを表示させる方法について説明します。 次に、テストでアサートを使用する方法、テストが予期しない例外を処理する方法、および予期される例外をテストする方法を示します。







最後に、多くのテストがあります。 この方法で、テストをクラス、モジュール、およびディレクトリに整理する方法を学習します。 次に、マーカーを使用して実行するテストをマークする方法を示し、組み込みマーカーがどのようにテストをスキップしてテストをマークし、失敗を期待できるかについて説明します。 最後に、テストのパラメーター化について説明します。これにより、異なるデータでテストを呼び出すことができます。







トランスレーター注: Python 3.5または3.6を使用している場合、第2章のテストを実行すると、このようなメッセージが表示される場合があります。



この問題は、 ...\code\tasks_proj\src\tasks\tasksdb_tinydb.py



し、タスクパッケージを再インストールすることで処理されます。

 $ cd /path/to/code $ pip install ./tasks_proj/`
      
      







モジュールのeids



doc_ids



名前付きパラメーターeid



doc_id



eid



doc_ids



するeids



ます...\code\tasks_proj\src\tasks\tasksdb_tinydb.py







説明#83783



参照してください


パッケージテスト



Pythonパッケージのテスト関数の作成方法を学習するには、xiiページのTasksプロジェクトで説明されているサンプルTasksプロジェクトを使用します。 タスクは、同じタスク名のコマンドラインツールを含むPythonパッケージです。







付録4、Pythonプロジェクトのパッケージ化と配布(175ページ)では、小規模なチーム内でローカルに、またはPyPIを介してグローバルにプロジェクトを配布する方法について説明します。そのため、詳細については説明しません。 ただし、Tasksプロジェクトの内容と、このプロジェクトのテスト履歴にさまざまなファイルがどのように適合するかを簡単に見てみましょう。







Tasksプロジェクトのファイル構造は次のとおりです。







 tasks_proj/ ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.py ├── src │ └── tasks │ ├── __init__.py │ ├── api.py │ ├── cli.py │ ├── config.py │ ├── tasksdb_pymongo.py │ └── tasksdb_tinydb.py └── tests ├── conftest.py ├── pytest.ini ├── func │ ├── __init__.py │ ├── test_add.py │ └── ... └── unit ├── __init__.py ├── test_task.py └── ...
      
      





プロジェクトの完全なリスト(テストファイルの完全なリストを除く)を含めて、テストがプロジェクトの他の部分にどのように適合するかを示し、テストの鍵となるいくつかのファイル、つまりconftest.py、pytest.ini 、さまざまな__init__.py



ファイルとsetup.py







すべてのテストはテストに保存され、 srcのパッケージソースファイルとは別に保存されます。 これは完全な要件ではありませんが、ベストプラクティスです。







CHANGELOG.rst、LICENSE、README.rst、MANIFEST.in 、およびsetup.pyのすべてのトップレベルファイルについては、付録4 Pythonプロジェクトのパッケージ化と配布、ページ175で詳しく説明します。配布の構築にはsetup.pyが重要ですが、パッケージから、およびパッケージをローカルにインストールして、パッケージをインポートできるようにするため。







機能テストと単体テストは、独自のカタログに分割されます。 これは任意の決定であり、必要ではありません。 ただし、テストファイルをいくつかのディレクトリに整理すると、テストのサブセットを簡単に実行できます。 機能テストはシステムの機能を意図的に変更した場合にのみ機能し、リファクタリングや実装の変更中にユニットテストが機能しない場合があるため、機能テストと単体テストを分離するのが好きです。







プロジェクトには2種類の__init__.py



ファイルが含まれています: src/



ディレクトリにあるファイルとtests/



ファイルです。 src/tasks/__init__.py



は、ディレクトリがパッケージであることをPythonに伝えます。 また、誰かがimport tasks



使用するときに、パッケージのメインインターフェイスとして機能します。 api.py



から特定の関数をインポートするコードが含まれているため、 cli.py



とテストファイルはtasks.add()



を実行する代わりに、 tasks.add()



などのパッケージ関数にアクセスできます。 ファイルtests/func/__init__.py



およびtests/unit/__init__.py



は空です。 pytestに1つのディレクトリに移動して、テストディレクトリのルートとpytest.ini



ファイルを見つけるようにpytest.ini



ます。







pytest.ini



ファイルはオプションです。 プロジェクト全体の一般的なpytest構成が含まれています。 あなたのプロジェクトはそれらのうちの一つ以上を持たないはずです。 常に使用されるパラメーターのリストを設定するなど、pytestの動作を変更するディレクティブを含めることができます。 pytest.ini



すべてについては、113ページの第6章「構成」で学習します。







conftest.pyファイルもオプションです。 pytestは「ローカルプラグイン」と見なされ、フック関数とフィクスチャを含む場合があります。 フック関数は、pytestランタイムの一部にコードを貼り付けて、pytestの動作を変更する方法です。 フィクスチャは、テスト関数の前後に実行されるセットアップおよびティアダウン関数であり、テストで使用されるリソースとデータを表すために使用できます。 (フィクスチャについては、第3章pytestフィクスチャ(49ページ)および第4章組み込みフィクスチャ(71ページ)で説明し、フック関数については、第5章「プラグイン」(95ページ)で説明します。)で使用されるフック関数およびフィクスチャいくつかのサブディレクトリのテストは、tests / conftest.pyに含まれている必要があります。 conftest.pyファイルをいくつか持つことができます。 たとえば、テストに1つ、各テストサブディレクトリに1つを持つことができます。







まだ行っていない場合は、このプロジェクトのソースコードのコピーを本のWebサイトからダウンロードできます。 または、同様の構造でプロジェクトに取り組むことができます。







test_task.pyは次のとおりです。







ch2 / tasks_proj / tests / unit / test_task.py


 """Test the Task data type.""" # -*- coding: utf-8 -*- from tasks import Task def test_asdict(): """_asdict()   .""" t_task = Task('do something', 'okken', True, 21) t_dict = t_task._asdict() expected = {'summary': 'do something', 'owner': 'okken', 'done': True, 'id': 21} assert t_dict == expected def test_replace(): """replace ()      .""" t_before = Task('finish book', 'brian', False) t_after = t_before._replace(id=10, done=True) t_expected = Task('finish book', 'brian', True, 10) assert t_after == t_expected def test_defaults(): """        .""" t1 = Task() t2 = Task(None, None, False, None) assert t1 == t2 def test_member_access(): """ .field  namedtuple.""" t = Task('buy milk', 'brian') assert t.summary == 'buy milk' assert t.owner == 'brian' assert (t.done, t.id) == (False, None)
      
      





test_task.pyファイルには、次のインポートステートメントが含まれています。







 from tasks import Task
      
      





テストでタスクをインポートしたり、タスクから何かをインポートしたりする最良の方法は、pipを使用してタスクをローカルにインストールすることです。 これは、pipを直接呼び出すためのsetup.pyファイルがあるため可能です。







pip install .



を実行してタスクをpip install .



またはpip install -e .



tasks_projディレクトリから。 または、1レベル上のディレクトリからpip install -e tasks_proj



を実行する別のオプション:







 $ cd /path/to/code $ pip install ./tasks_proj/ $ pip install --no-cache-dir ./tasks_proj/ Processing ./tasks_proj Collecting click (from tasks==0.1.0) Downloading click-6.7-py2.py3-none-any.whl (71kB) ... Collecting tinydb (from tasks==0.1.0) Downloading tinydb-3.4.0.tar.gz Collecting six (from tasks==0.1.0) Downloading six-1.10.0-py2.py3-none-any.whl Installing collected packages: click, tinydb, six, tasks Running setup.py install for tinydb ... done Running setup.py install for tasks ... done Successfully installed click-6.7 six-1.10.0 tasks-0.1.0 tinydb-3.4.0
      
      





タスクのテストのみを実行する場合は、このコマンドで実行できます。 タスクのインストール中にソースコードを変更できるようにする場合は、-eオプション(編集可能な「編集可能」の場合)を指定してインストールを使用する必要があります。







 $ pip install -e ./tasks_proj/ Obtaining file:///path/to/code/tasks_proj Requirement already satisfied: click in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Requirement already satisfied: tinydb in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Requirement already satisfied: six in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Installing collected packages: tasks Found existing installation: tasks 0.1.0 Uninstalling tasks-0.1.0: Successfully uninstalled tasks-0.1.0 Running setup.py develop for tasks Successfully installed tasks
      
      





次に、テストを実行してみます。







 $ cd /path/to/code/ch2/tasks_proj/tests/unit $ pytest test_task.py ===================== test session starts ====================== collected 4 items test_task.py .... =================== 4 passed in 0.01 seconds ===================
      
      





インポートが機能しました! 他のテストでインポートタスクを安全に使用できるようになりました。 それでは、いくつかのテストを書きましょう。







assert文を使用する



テスト関数を記述するとき、通常のPythonステートメントのアサートは、テストの失敗を報告するための主要なツールです。 pytestでのこの単純さは素晴らしいです。 これが、多くの開発者が他のフレームワークの上でpytestを使用する理由です。







他のテストプラットフォームを使用した場合は、おそらくさまざまなアサートヘルパー関数を見たでしょう。 たとえば、次はアサートおよびアサートヘルパー関数のいくつかの形式のリストです。







pytest ユニットテスト
何かを主張する assertTrue(何か)
アサートa == b assertEqual(a、b)
アサートa <= b assertLessEqual(a、b)
... ...


pytestでは、任意の式でassert <expression>を使用できます。 式がブールに変換されたときにFalseと評価された場合、テストは失敗します。







pytestには、assertの呼び出しをインターセプトし、ステートメントが失敗した理由の詳細を伝えることができるものに置き換える、assert rewritingという関数が含まれています。 いくつかのステートメントエラーを見ると、この書き換えがどれほど便利かを見てみましょう。







ch2 / tasks_proj / tests / unit / test_task_fail.py


 """ the Task type    .""" from tasks import Task def test_task_equality(): """     .""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') assert t1 == t2 def test_dict_equality(): """ ,   dicts,    .""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() assert t1_dict == t2_dict
      
      





これらのテストはすべて失敗しますが、トレース内の情報は興味深いものです。







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\unit>pytest test_task_fail.py ============================= test session starts ============================= collected 2 items test_task_fail.py FF ================================== FAILURES =================================== _____________________________ test_task_equality ______________________________ def test_task_equality(): """Different tasks should not be equal.""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') > assert t1 == t2 E AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None) E At index 0 diff: 'sit there' != 'do something' E Use -v to get the full diff test_task_fail.py:9: AssertionError _____________________________ test_dict_equality ______________________________ def test_dict_equality(): """Different tasks compared as dicts should not be equal.""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() > assert t1_dict == t2_dict E AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)]) E Omitting 3 identical items, use -vv to show E Differing items: E {'owner': 'okken'} != {'owner': 'okkem'} E Use -v to get the full diff test_task_fail.py:16: AssertionError ========================== 2 failed in 0.30 seconds ===========================
      
      





わあ! これは多くの情報です。 失敗したテストごとに、失敗インジケータを使用して正確なエラー文字列が表示されます。 行Eは、アサートの失敗に関する追加情報を示し、何が問題であったかを理解するのに役立ちます。







私は意図的にtest_task_equality()



に2つの不一致を入れましたが、前のコードでは最初の不一致のみが表示されました。 エラーメッセージに-v



れているように、 -v



フラグを使用して再試行してください。







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\unit>pytest -v test_task_fail.py ============================= test session starts ============================= collected 2 items test_task_fail.py::test_task_equality FAILED test_task_fail.py::test_dict_equality FAILED ================================== FAILURES =================================== _____________________________ test_task_equality ______________________________ def test_task_equality(): """Different tasks should not be equal.""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') > assert t1 == t2 E AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None) E At index 0 diff: 'sit there' != 'do something' E Full diff: E - Task(summary='sit there', owner='brian', done=False, id=None) E ? ^^^ ^^^ ^^^^ E + Task(summary='do something', owner='okken', done=False, id=None) E ? +++ ^^^ ^^^ ^^^^ test_task_fail.py:9: AssertionError _____________________________ test_dict_equality ______________________________ def test_dict_equality(): """Different tasks compared as dicts should not be equal.""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() > assert t1_dict == t2_dict E AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)]) E Omitting 3 identical items, use -vv to show E Differing items: E {'owner': 'okken'} != {'owner': 'okkem'} E Full diff: E {'summary': 'make sandwich', E - 'owner': 'okken', E ? ^... E E ...Full output truncated (5 lines hidden), use '-vv' to show test_task_fail.py:16: AssertionError ========================== 2 failed in 0.28 seconds ===========================
      
      





まあ、私はそれがすごくクールだと思う! pytestは両方の違いを見つけることができるだけでなく、これらの違いがどこにあるかを正確に示しました。 この例では、等価アサートのみを使用しています。 pytest.orgには、驚異的なトレースデバッグ情報を含むアサートステートメントのバリエーションが多数あります。







予想される例外



Tasks APIのいくつかの場所で例外が発生する可能性があります。 tasks / api.pyにある関数を簡単に見てみましょう。







 def add(task): # type: (Task) -\> int def get(task_id): # type: (int) -\> Task def list_tasks(owner=None): # type: (str|None) -\> list of Task def count(): # type: (None) -\> int def update(task_id, task): # type: (int, Task) -\> None def delete(task_id): # type: (int) -\> None def delete_all(): # type: () -\> None def unique_id(): # type: () -\> int def start_tasks_db(db_path, db_type): # type: (str, str) -\> None def stop_tasks_db(): # type: () -\> None
      
      





cli.pyのCLIコードとapi.pyのAPIコードの間には、API関数に渡されるタイプに関する合意があります。 API呼び出しは、型が正しくない場合に例外が発生することを期待する場所です。 これらの関数が正しく呼び出されない場合に例外がスローされるようにするには、テスト関数で間違った型を使用して、意図的にTypeError例外をスローし、pytest.raises(予想される例外)で使用します。







ch2 / tasks_proj / tests / func / test_api_exceptions.py


 """    -   API.""" import pytest import tasks def test_add_raises(): """add()       param.""" with pytest.raises(TypeError): tasks.add(task='not a Task object')
      
      





test_add_raises()



で、 pytest.raises(TypeError)



:次のコードブロックのすべてがTypeError例外をスローする必要があることをステートメントが報告します。 例外が発生しない場合、テストは失敗します。 テストで別の例外が発生した場合、失敗します。







test_add_raises()



で例外のタイプをチェックしました。 除外オプションを確認することもできます。 start_tasks_db(db_path, db_type)



場合start_tasks_db(db_path, db_type)



が文字列であるだけでなく、実際には 'tiny'または 'mongo'である必要があります。 excinfoを追加することで、例外メッセージが正しいことを確認できます。







ch2 / tasks_proj / tests / func / test_api_exceptions.py


 def test_start_tasks_db_raises(): """,     .""" with pytest.raises(ValueError) as excinfo: tasks.start_tasks_db('some/great/path', 'mysql') exception_msg = excinfo.value.args[0] assert exception_msg == "db_type must be a 'tiny' or 'mongo'"
      
      





これにより、この例外をより詳しく調べることができます。 asの後の変数名(この場合はexcinfo)には例外情報が入力され、ExceptionInfo型になります。







この場合、最初の(そして唯一の)例外パラメーターが文字列と一致することを確認する必要があります。







テスト機能のマーキング



pytestは、テスト機能にマーカーを配置するクールなメカニズムを提供します。 テストには複数のマーカーを含めることができ、マーカーは複数のテストに含めることができます。







マーカーは、実際に動作を確認した後に意味を持ちます。 システムに深刻なギャップがあるかどうかを知るために、テストのサブセットを簡単な「煙テスト」として実行したいとします。 慣例により、Smokeテストは包括的で厳密なテストスイートではなく、選択したサブセットであり、すばやく実行して、システムのすべての部分の健全性の適切な全体像を開発者に提供できます。







タスクプロジェクトにスモークテストスイートを追加するには、一部のテストに@mark.pytest.smoke



を追加する必要があります。 いくつかのtest_api_exceptions.py



テストに追加してみましょう( smokeおよびgetマーカーはpytestに組み込まれていないことに注意してください。私はそれらを思いついたばかりです)。







ch2 / tasks_proj / tests / func / test_api_exceptions.py


 @pytest.mark.smoke def test_list_raises(): """list()       param.""" with pytest.raises(TypeError): tasks.list_tasks(owner=123) @pytest.mark.get @pytest.mark.smoke def test_get_raises(): """get()       param.""" with pytest.raises(TypeError): tasks.get(task_id='123')
      
      





-m marker_name



マークされたテストのみを実行しましょう:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests>cd func (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_list_raises PASSED test_api_exceptions.py::test_get_raises PASSED ============================= 5 tests deselected ============================== =================== 2 passed, 5 deselected in 0.18 seconds ==================== (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func> (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_get_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ====================
      
      





-v



--verbose



短縮形であり、実行中のテストの名前を確認できることを忘れないでください。 -m 'smoke'を使用すると、@ pytest.mark.smokeというラベルの両方のテストが実行されます。







-m



'get'を使用すると、 @pytest.mark.get



マークされた1つのテストが実行されます。 とても簡単です。







すべてが奇跡と奇跡になります! -m



後の式はand



or



or



not



使用not



て複数のマーカーnot



結合できます。







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke and get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_get_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ====================
      
      





このテストはsmoke



でのみ行い、マーカーをget



not



を使用できます:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke and not get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_list_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ====================
      
      





-m 'smoke and not get'



追加すると、 @pytest.mark.smoke



フラグが付けられ、 @pytest.mark.smoke



ではフラグが付けられていないテストが選択されました。







煙テスト塗りつぶし



以前のテストは、まだ合理的な一連のsmoke test



ようには見えません。 実際にはデータベースに触れず、タスクを追加しませんでした。 もちろん、 smoke test



はこれを行う必要があります。







タスクの追加を検討するいくつかのテストを追加し、そのうちの1つをスモークテストスイートの一部として使用しましょう。







ch2 / tasks_proj / tests / func / test_add.py


 """  API tasks.add ().""" import pytest import tasks from tasks import Task def test_add_returns_valid_id(): """tasks.add(valid task)    .""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) assert isinstance(task_id, int) @pytest.mark.smoke def test_added_task_has_id_set(): """,   task_id  tasks.add().""" # GIVEN an initialized tasks db # AND a new task is added new_task = Task('sit in chair', owner='me', done=True) task_id = tasks.add(new_task) # WHEN task is retrieved task_from_db = tasks.get(task_id) # THEN task_id matches id field assert task_from_db.id == task_id
      
      





これらのテストは両方とも、初期化されたタスクデータベースに関するGIVENコメントを持っていますが、テストには初期化されたデータベースはありません。 テスト前にデータベースを初期化し、テスト後にクリーンアップするためにフィクスチャを定義できます。







ch2 / tasks_proj / tests / func / test_add.py


 @pytest.fixture(autouse=True) def initialized_tasks_db(tmpdir): """Connect to db before testing, disconnect after.""" # Setup : start db tasks.start_tasks_db(str(tmpdir), 'tiny') yield #    # Teardown : stop db tasks.stop_tasks_db()
      
      





この例で使用されるフィクスチャtmpdirは、組み込みのフィクスチャです。 ビルトインフィクスチャについては、第4章「ビルトインフィクスチャ」(71ページ)ですべて学習し、独自のフィクスチャの記述方法と、その動作方法については、ここで使用するautouseパラメータを含め、第3章「pytestフィクスチャ」(49ページ)で学習します。







テストで使用された自動使用は、このファイルのすべてのテストがフィクスチャを使用することを示しています。 yield



前のコードは、各テストの前に実行されます。 yield



後のコードはテスト後に実行されます。 必要に応じて、yieldはテストにデータを返すことができます。 これ以降の章ではすべてを検討しますが、ここではテスト用にデータベースを何らかの形で構成する必要があります。そのため、このデバイスを待つ必要はありません(もちろんフィクスチャです!)。 (pytestは、unittestやnoseで使用されるような、旧式のセットアップおよびティアダウン機能もサポートしていますが、それほど興味深いものではありません。ただし、興味がある場合は、付録5、xUnitフィクスチャ、183ページで説明します。)







とりあえずフィクスチャの説明を延期し、プロジェクトの最初に進み、 スモークテストスイートを実行しましょう。







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>cd .. (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests>cd .. (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -m "smoke" ============================= test session starts ============================= collected 56 items tests/func/test_add.py::test_added_task_has_id_set PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED ============================= 53 tests deselected ============================= =================== 3 passed, 53 deselected in 0.49 seconds ===================
      
      





, .







(Skipping Tests)



, , . 31 , pytest : skip



, skipif



, xfail



. skip



skipif



, -xfail



.







skip



skipif



, . , , , tasks.unique_id()



. ? , ?







-, (, initialized_tasks_db



; ):







ch2/tasks_proj/tests/func/ test_unique_id_1.py





 """Test tasks.unique_id().""" import pytest import tasks def test_unique_id(): """ unique_id ()     .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2
      
      





:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_1.py ============================= test session starts ============================= collected 1 item test_unique_id_1.py F ================================== FAILURES =================================== _______________________________ test_unique_id ________________________________ def test_unique_id(): """Calling unique_id() twice should return different numbers.""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() > assert id_1 != id_2 E assert 1 != 1 test_unique_id_1.py:11: AssertionError ========================== 1 failed in 0.30 seconds ===========================
      
      





うん , . API , , docstring """Return an integer that does not exist in the db.""", , DB . . , :







ch2/tasks_proj/tests/func/ test_unique_id_2.py





 @pytest.mark.skip(reason='misunderstood the API') def test_unique_id_1(): """ unique_id ()     .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2 def test_unique_id_2(): """unique_id()    id.""" ids = [] ids.append(tasks.add(Task('one'))) ids.append(tasks.add(Task('two'))) ids.append(tasks.add(Task('three'))) #   id uid = tasks.unique_id() # ,        assert uid not in ids
      
      





, , , @pytest..skip()



.







:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_2.py ============================= test session starts ============================= collected 2 items test_unique_id_2.py::test_unique_id_1 SKIPPED test_unique_id_2.py::test_unique_id_2 PASSED ===================== 1 passed, 1 skipped in 0.19 seconds =====================
      
      





, - , , 0.2.0 . skipif:







ch2/tasks_proj/tests/func/ test_unique_id_3.py





 @pytest.mark.skipif(tasks.__version__ < '0.2.0', reason='not supported until version 0.2.0') def test_unique_id_1(): """ unique_id ()     .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2
      
      





, skipif()



, Python. , , . skip , skipif . skip , skipif . ( reason ) skip , skipif xfail . :







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py s. ===================== 1 passed, 1 skipped in 0.20 seconds =====================
      
      





s.



, (skipped), (passed). , - -v



:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py::test_unique_id_1 SKIPPED test_unique_id_3.py::test_unique_id_2 PASSED ===================== 1 passed, 1 skipped in 0.19 seconds =====================
      
      





. -rs



:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -rs test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py s. =========================== short test summary info =========================== SKIP [1] func\test_unique_id_3.py:8: not supported until version 0.2.0 ===================== 1 passed, 1 skipped in 0.22 seconds =====================
      
      





-r chars



:







 $ pytest --help ... -r chars show extra test summary info as specified by chars (     ,  ) (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed, (p)passed, (P)passed with output, (a)all except pP. ...
      
      





, .









skip



skipif



, . xfail



pytest , , . unique_id ()



, xfail



:







ch2/tasks_proj/tests/func/ test_unique_id_4.py





 @pytest.mark.xfail(tasks.__version__ < '0.2.0', reason='not supported until version 0.2.0') def test_unique_id_1(): """ unique_id()     .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2 @pytest.mark.xfail() def test_unique_id_is_a_duck(): """ xfail.""" uid = tasks.unique_id() assert uid == 'a duck' @pytest.mark.xfail() def test_unique_id_not_a_duck(): """ xpass.""" uid = tasks.unique_id() assert uid != 'a duck'
      
      





Running this shows:







, , xfail



. == vs.! =. .







:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_4.py ============================= test session starts ============================= collected 4 items test_unique_id_4.py xxX. =============== 1 passed, 2 xfailed, 1 xpassed in 0.36 seconds ================
      
      





X XFAIL, « ( expected to fail )». X XPASS «, , ( expected to fail but passed. )».







--verbose



:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_4.py ============================= test session starts ============================= collected 4 items test_unique_id_4.py::test_unique_id_1 xfail test_unique_id_4.py::test_unique_id_is_a_duck xfail test_unique_id_4.py::test_unique_id_not_a_duck XPASS test_unique_id_4.py::test_unique_id_2 PASSED =============== 1 passed, 2 xfailed, 1 xpassed in 0.36 seconds ================
      
      





pytest , , , xfail



, FAIL. pytest.ini :







 [pytest] xfail_strict=true
      
      





pytest.ini 6, , . 113.









, ​​ . . , , . , . . .







A Single Directory



, pytest :







 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest tests\func --tb=no ============================= test session starts ============================= collected 50 items tests\func\test_add.py .. tests\func\test_add_variety.py ................................ tests\func\test_api_exceptions.py ....... tests\func\test_unique_id_1.py F tests\func\test_unique_id_2.py s. tests\func\test_unique_id_3.py s. tests\func\test_unique_id_4.py xxX. ==== 1 failed, 44 passed, 2 skipped, 2 xfailed, 1 xpassed in 1.75 seconds =====
      
      





, -v



, .







 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v tests\func --tb=no ============================= test session starts =============================
      
      





...







 collected 50 items tests\func\test_add.py::test_add_returns_valid_id PASSED tests\func\test_add.py::test_added_task_has_id_set PASSED tests\func\test_add_variety.py::test_add_1 PASSED tests\func\test_add_variety.py::test_add_2[task0] PASSED tests\func\test_add_variety.py::test_add_2[task1] PASSED tests\func\test_add_variety.py::test_add_2[task2] PASSED tests\func\test_add_variety.py::test_add_2[task3] PASSED tests\func\test_add_variety.py::test_add_3[sleep-None-False] PASSED ... tests\func\test_unique_id_2.py::test_unique_id_1 SKIPPED tests\func\test_unique_id_2.py::test_unique_id_2 PASSED ... tests\func\test_unique_id_4.py::test_unique_id_1 xfail tests\func\test_unique_id_4.py::test_unique_id_is_a_duck xfail tests\func\test_unique_id_4.py::test_unique_id_not_a_duck XPASS tests\func\test_unique_id_4.py::test_unique_id_2 PASSED ==== 1 failed, 44 passed, 2 skipped, 2 xfailed, 1 xpassed in 2.05 seconds =====
      
      





, .







File/Module



, , pytest:







 $ cd /path/to/code/ch2/tasks_proj $ pytest tests/func/test_add.py =========================== test session starts =========================== collected 2 items tests/func/test_add.py .. ======================== 2 passed in 0.05 seconds =========================
      
      





.









, ::



:







 $ cd /path/to/code/ch2/tasks_proj $ pytest -v tests/func/test_add.py::test_add_returns_valid_id =========================== test session starts =========================== collected 3 items tests/func/test_add.py::test_add_returns_valid_id PASSED ======================== 1 passed in 0.02 seconds =========================
      
      





-v



, , .







Test Class



Here's an example:







— , .

以下に例を示します。







ch2/tasks_proj/tests/func/ test_api_exceptions.py





 class TestUpdate(): """    tasks.update().""" def test_bad_id(self): """non-int id   excption.""" with pytest.raises(TypeError): tasks.update(task_id={'dict instead': 1}, task=tasks.Task()) def test_bad_task(self): """A non-Task task   excption.""" with pytest.raises(TypeError): tasks.update(task_id=1, task='not a task')
      
      





, update()



, . , , ::



, :







 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v tests/func/test_api_exceptions.py::TestUpdate ============================= test session starts ============================= collected 2 items tests\func\test_api_exceptions.py::TestUpdate::test_bad_id PASSED tests\func\test_api_exceptions.py::TestUpdate::test_bad_task PASSED ========================== 2 passed in 0.12 seconds ===========================
      
      





A Single Test Method of a Test Class



, — ::



:







 $ cd /path/to/code/ch2/tasks_proj $ pytest -v tests/func/test_api_exceptions.py::TestUpdate::test_bad_id ===================== test session starts ====================== collected 1 item tests/func/test_api_exceptions.py::TestUpdate::test_bad_id PASSED =================== 1 passed in 0.03 seconds ===================
      
      





,



, , , , . , pytest -v



.




-k



, . and



, or



not



. , _raises



:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -k _raises ============================= test session starts ============================= collected 56 items tests/func/test_api_exceptions.py::test_add_raises PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED tests/func/test_api_exceptions.py::test_delete_raises PASSED tests/func/test_api_exceptions.py::test_start_tasks_db_raises PASSED ============================= 51 tests deselected ============================= =================== 5 passed, 51 deselected in 0.54 seconds ===================
      
      





and



not



test_delete_raises()



:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -k "_raises and not delete" ============================= test session starts ============================= collected 56 items tests/func/test_api_exceptions.py::test_add_raises PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED tests/func/test_api_exceptions.py::test_start_tasks_db_raises PASSED ============================= 52 tests deselected ============================= =================== 4 passed, 52 deselected in 0.44 seconds ===================
      
      





, , , -k



. , , .







[Parametrized Testing]:



, , . . - pytest, - .







, , add()



:







ch2/tasks_proj/tests/func/ test_add_variety.py





 """  API tasks.add().""" import pytest import tasks from tasks import Task def test_add_1(): """tasks.get ()  id,   add() works.""" task = Task('breathe', 'BRIAN', True) task_id = tasks.add(task) t_from_db = tasks.get(task_id) # ,  ,    assert equivalent(t_from_db, task) def equivalent(t1, t2): """   .""" #  ,   id return ((t1.summary == t2.summary) and (t1.owner == t2.owner) and (t1.done == t2.done)) @pytest.fixture(autouse=True) def initialized_tasks_db(tmpdir): """    ,  .""" tasks.start_tasks_db(str(tmpdir), 'tiny') yield tasks.stop_tasks_db()
      
      





tasks id



None



. id



. ==



, , . equivalent()



, id



. autouse



, , . , :







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_1 ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_1 PASSED ========================== 1 passed in 0.69 seconds ===========================
      
      





. , . , ? . @pytest.mark.parametrize(argnames, argvalues)



, :







ch2/tasks_proj/tests/func/ test_add_variety.py





 @pytest.mark.parametrize('task', [Task('sleep', done=True), Task('wake', 'brian'), Task('breathe', 'BRIAN', True), Task('exercise', 'BrIaN', False)]) def test_add_2(task): """    .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)
      
      





parametrize()



— — 'task', . — , Task. pytest :







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_2 ============================= test session starts ============================= collected 4 items test_add_variety.py::test_add_2[task0] PASSED test_add_variety.py::test_add_2[task1] PASSED test_add_variety.py::test_add_2[task2] PASSED test_add_variety.py::test_add_2[task3] PASSED ========================== 4 passed in 0.69 seconds ===========================
      
      





parametrize()



. , , :







ch2/tasks_proj/tests/func/ test_add_variety.py





 @pytest.mark.parametrize('summary, owner, done', [('sleep', None, False), ('wake', 'brian', False), ('breathe', 'BRIAN', True), ('eat eggs', 'BrIaN', False), ]) def test_add_3(summary, owner, done): """    .""" task = Task(summary, owner, done) task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)
      
      





, pytest, , :







 (venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_3 ============================= test session starts ============================= platform win32 -- Python 3.5.2, pytest-3.5.1, py-1.5.3, pluggy-0.6.0 -- cachedir: ..\.pytest_cache rootdir: ...\bopytest-code\code\ch2\tasks_proj\tests, inifile: pytest.ini collected 4 items test_add_variety.py::test_add_3[sleep-None-False] PASSED [ 25%] test_add_variety.py::test_add_3[wake-brian-False] PASSED [ 50%] test_add_variety.py::test_add_3[breathe-BRIAN-True] PASSED [ 75%] test_add_variety.py::test_add_3[eat eggs-BrIaN-False] PASSED [100%] ========================== 4 passed in 0.37 seconds ===========================
      
      





, , pytest, :







 (venv35) c:\BOOK\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_3[sleep-None-False] ============================= test session starts ============================= test_add_variety.py::test_add_3[sleep-None-False] PASSED [100%] ========================== 1 passed in 0.22 seconds ===========================
      
      





, :







 (venv35) c:\BOOK\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v "test_add_variety.py::test_add_3[eat eggs-BrIaN-False]" ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_3[eat eggs-BrIaN-False] PASSED [100%] ========================== 1 passed in 0.56 seconds ===========================
      
      





, :







ch2/tasks_proj/tests/func/ test_add_variety.py





 tasks_to_try = (Task('sleep', done=True), Task('wake', 'brian'), Task('wake', 'brian'), Task('breathe', 'BRIAN', True), Task('exercise', 'BrIaN', False)) @pytest.mark.parametrize('task', tasks_to_try) def test_add_4(task): """ .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)
      
      





. :







 (venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_4 ============================= test session starts ============================= collected 5 items test_add_variety.py::test_add_4[task0] PASSED [ 20%] test_add_variety.py::test_add_4[task1] PASSED [ 40%] test_add_variety.py::test_add_4[task2] PASSED [ 60%] test_add_variety.py::test_add_4[task3] PASSED [ 80%] test_add_variety.py::test_add_4[task4] PASSED [100%] ========================== 5 passed in 0.34 seconds ===========================
      
      





, . , ids parametrize()



, . ids



, . , tasks_to_try



, :







ch2/tasks_proj/tests/func/ test_add_variety.py





 task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) for t in tasks_to_try] @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) def test_add_5(task): """Demonstrate ids.""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)
      
      





, :







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_5 ============================= test session starts ============================= collected 5 items test_add_variety.py::test_add_5[Task(sleep,None,True)] PASSED test_add_variety.py::test_add_5[Task(wake,brian,False)0] PASSED test_add_variety.py::test_add_5[Task(wake,brian,False)1] PASSED test_add_variety.py::test_add_5[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)] PASSED ========================== 5 passed in 0.45 seconds ===========================
      
      





:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v "test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)]" ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)] PASSED ========================== 1 passed in 0.21 seconds ===========================
      
      





; shell. parametrize()



. :







ch2/tasks_proj/tests/func/ test_add_variety.py





 @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) class TestAdd(): """   .""" def test_equivalent(self, task): """ ,   .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task) def test_valid_id(self, task): """          .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert t_from_db.id == task_id
      
      





:







 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::TestAdd ============================= test session starts ============================= collected 10 items test_add_variety.py::TestAdd::test_equivalent[Task(sleep,None,True)] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(wake,brian,False)0] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(wake,brian,False)1] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(exercise,BrIaN,False)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(sleep,None,True)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(wake,brian,False)0] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(wake,brian,False)1] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(exercise,BrIaN,False)] PASSED ========================== 10 passed in 1.16 seconds ==========================
      
      





, @pytest.mark.parametrize()



. pytest.param(<value\>, id="something")



:







:







 (venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func $ pytest -v test_add_variety.py::test_add_6 ======================================== test session starts ========================================= collected 3 items test_add_variety.py::test_add_6[just summary] PASSED [ 33%] test_add_variety.py::test_add_6[summary\owner] PASSED [ 66%] test_add_variety.py::test_add_6[summary\owner\done] PASSED [100%] ================================ 3 passed, 6 warnings in 0.35 seconds ================================
      
      





, id



.









  1. , task_proj



    , - , pip install /path/to/tasks_proj



    .
  2. .
  3. pytest .
  4. pytest , tasks_proj/tests/func



    . pytest , . . , ?
  5. xfail , pytest tests .
  6. tasks.count()



    , . API , , , .
  7. ? test_api_exceptions.py



    . , . ( api.py



    .)


次は何ですか



pytest . , , , . initialized_tasks_db



. / .







また、複数のテスト機能が同じ設定を使用できるように、共通コードを分離することもできます。次の章では、フィクスチャpytestの素晴らしい世界に深く入り込みます。







戻る 次へ








All Articles