ベスパリブ

プログラミングを主とした日記・備忘録です。ベスパ持ってないです。

「テスト駆動Python」写経 CHAPTER1

www.amazon.co.jp

1 はじめてのpytest

pytestを使ったテスト駆動開発についての本です。

pytestを実行する

test_one.py

def test_passing():
    assert (1, 2, 3) == (1, 2, 3)

テストを実行します。

$ pytest test_one.py
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 1 item                                                                                                                                                                                                                      

test_one.py .                                                                                                                                                                                                                  [100%] 

===================================================================================================== 1 passed in 0.01 seconds ====================================================================================================== 

より詳細に知りたい場合は、-vオプションを追加します。

$ pytest -v test_one.py
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\pytestx\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 1 item                                                                                                                                                                                                                      

test_one.py::test_passing PASSED                                                                                                                                                                                               [100%] 

===================================================================================================== 1 passed in 0.01 seconds ====================================================================================================== 

test_two.py

def test_falling():
    assert(1,2,3) == (3,2,1)

テストを実行します。

$ pytest -v test_two.py
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\pytestx\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 1 item                                                                                                                                                                                                                      

test_two.py::test_falling FAILED                                                                                                                                                                                               [100%] 

============================================================================================================= FAILURES ============================================================================================================== 
___________________________________________________________________________________________________________ test_falling ____________________________________________________________________________________________________________ 

    def test_falling():
>       assert(1,2,3) == (3,2,1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Full diff:
E         - (1, 2, 3)
E         ?  ^     ^
E         + (3, 2, 1)
E         ?  ^     ^

test_two.py:2: AssertionError
===================================================================================================== 1 failed in 0.04 seconds ====================================================================================================== 

pytestはファイルやディレクトリを指定しない場合、現在のディレクトリとそのサブディレクトリでテストを検索し実行します。pytestが実行されるのはtest_で始まるファイルと、_testで終わるファイル。

# ファイルを指定してテスト実行
$ pytest -v test_one.py

# 複数のファイルを指定してテスト実行
$ pytest -v tasks/test_three.py tasks/test_four.py

# ディレクトリを指定してテスト実行
$ pytest -v tasks

# 現在のディレクトリ以下すべてテスト実行
$ pytest -v

pytestのテストディスカバリ

pytestの実行するテストを検索する部分を「テストディスカバリ(test discovery)」といいます。

pytestの命名規則に準拠した名前がついたものは、pytestのテストが実行されます。その命名規則は以下の3つ。

  1. 「test_*.py」または「*_test.py」というファイル名
  2. 「test_*」という名前のメソッドや関数
  3. 「Test*」という名前のクラス

test_one.pyという名前のファイルに、「hoge()」という名前の関数があってもそれはテストされない。

このテストディスカバリルールは変更することが可能。

テストを1つだけ実行する

ファイルの中の関数を指定して、テストしたい関数のみをテストすることができます。

$ pytest -v tasks/test_one.py::test_passing

pytestのオプション

-h, --help

$ pytest --help

pytestのヘルプです。

--collect-only

$ pytest --collect-only
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 6 items                                                                                                                                                                                                                     
<Module test_one.py>
  <Function test_passing>
<Module test_two.py>
  <Function test_falling>
<Module tasks/test_four.py>
  <Function test_asdict>
  <Function test_replace>
<Module tasks/test_three.py>
  <Function test_defaults>
  <Function test_member_access>

=================================================================================================== no tests ran in 0.02 seconds ==================================================================================================== 

実行されるテストを表示します。テスト実行前にチェックするのに役立ちます。

-k EXPRESSION

$ pytest -k "asdict or defaults" --collect-only

-k オプションは、実行するテスト関数を、式を使って検索できるようにするフィルタ。

# asdictまたはdefaultsと名前がつくやつを指定
$ pytest -k "asdict or defaults" --collect-only
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 6 items / 4 deselected / 2 selected                                                                                                                                                                                         
<Module tasks/test_four.py>
  <Function test_asdict>
<Module tasks/test_three.py>
  <Function test_defaults>

=================================================================================================== 4 deselected in 0.02 seconds ==================================================================================================== 

# asdictまたはdefaultsと名前がつくやつのみテスト実行
$ pytest -v -k "asdict or defaults"
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\pytestx\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 6 items / 4 deselected / 2 selected                                                                                                                                                                                         

tasks/test_four.py::test_asdict PASSED                                                                                                                                                                                         [ 50%] 
tasks/test_three.py::test_defaults FAILED                                                                                                                                                                                      [100%] 

============================================================================================================= FAILURES ============================================================================================================== 
___________________________________________________________________________________________________________ test_defaults ___________________________________________________________________________________________________________ 

    def test_defaults():
        """Using no parameters should invoke defaults."""
>       t1 = Task()
E       TypeError: __new__() missing 4 required positional arguments: 'summary', 'owner', 'done', and 'id'

tasks\test_three.py:17: TypeError
========================================================================================= 1 failed, 1 passed, 4 deselected in 0.04 seconds ========================================================================================== 

-m MARKEXPR

マーカーを使ってテスト関数の一部にマークを付けると、それらの関数をまとめて実行できるようになります。たとえば、それぞれ別ファイルに含まれているtest_replace()とtest_member_access()を実行するために、それらにマークをつけます。

マーカーには好きな名前をつけることができます。run_these_pleaseというマークをつけるには、以下のようにします。

import pytest
...
@pytest.mark.run_these_please
def test_member_access():
...

test_replace()にも同じマークをつけます。

# マークをつけた関数のみをテスト実行する
$ pytest -v -m run_these_please
======================================================================================================== test session starts ========================================================================================================
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\pytestx\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 6 items / 4 deselected / 2 selected                                                                                                                                                                                         

tasks/test_four.py::test_replace PASSED                                                                                                                                                                                        [ 50%] 
tasks/test_three.py::test_member_access FAILED                                                                                                                                                                                 [100%] 

============================================================================================================= FAILURES ============================================================================================================== 
________________________________________________________________________________________________________ test_member_access _________________________________________________________________________________________________________ 

    @pytest.mark.run_these_please
    def test_member_access():
        """Check .firld functionality of namedtuple."""
>       t = Task("buy milk", "brian")
E       TypeError: __new__() missing 2 required positional arguments: 'done' and 'id'

tasks\test_three.py:26: TypeError
========================================================================================================= warnings summary ========================================================================================================== 
C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\pytestx\lib\site-packages\_pytest\mark\structures.py:332
  C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\pytestx\lib\site-packages\_pytest\mark\structures.py:332: PytestUnknownMarkWarning: Unknown pytest.mark.run_these_please - is this a typo?  You can register custom marks 
to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================================================================================== 1 failed, 1 passed, 4 deselected, 1 warnings in 0.04 seconds ==================================================================================== 

-x, --exitfirst

pytest -x

pytestは通常、テスト関数でassertが失敗したり例外が発生したらテストの実行はそこで中止され、そのテストは失敗となります。そして次のテストを実行します。テストが失敗したら次のテストを実行せずにテストセッション全体を底で中止したい場合、この-xオプションを使います。

$ pytest -x
======================================================================================================== test session starts ========================================================================================================
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 6 items                                                                                                                                                                                                                     

test_one.py .                                                                                                                                                                                                                  [ 16%] 
test_two.py F

============================================================================================================= FAILURES ============================================================================================================== 
___________________________________________________________________________________________________________ test_falling ____________________________________________________________________________________________________________ 

    def test_falling():
>       assert(1,2,3) == (3,2,1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Use -v to get the full diff

test_two.py:2: AssertionError

========================================================================================== 1 failed, 1 passed, 1 warnings in 0.05 seconds =========================================================================================== 

test_one.pyは成功し、test_two.pyで失敗し、そこでテストセッション全体が終了していることがわかります。

--tb=no

$ pytest --tb=no

テスト失敗時のトレースバックをオフにします。失敗したテストを簡潔に表示したい場合につかいます。

$ pytest --tb=no
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 6 items                                                                                                                                                                                                                     

test_one.py .                                                                                                                                                                                                                  [ 16%] 
test_two.py F                                                                                                                                                                                                                  [ 33%] 
tasks\test_four.py ..                                                                                                                                                                                                          [ 66%] 
tasks\test_three.py FF                                                                                                                                                                                                         [100%] 

========================================================================================== 3 failed, 3 passed, 1 warnings in 0.05 seconds =========================================================================================== 

--maxfail=num

$ pytest --maxfail=2

「テストがnum個失敗したらテストセッション全体を中止する」ときに使います。 --maxfail=2 で、テストが2つ失敗したらテストセッション全体を中止します。

-s, --capture=method

# 出力のキャプチャを無効にし、出力が標準出力に書き出される
$ pytest -s
# または、
$ pytest --capture=no

デフォルトではprint()文は標準出力に表示されませんが、この-sを使うとprint()文を標準出力に出力します。

test_one.py

def test_passing():
    assert (1, 2, 3) == (1, 2, 3)
    print("End!")

テストを実行します。

# デフォルトでは、print()文は出力されない
$ pytest test_one.py
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 1 item                                                                                                                                                                                                                      

test_one.py .                                                                                                                                                                                                                  [100%] 

===================================================================================================== 1 passed in 0.02 seconds ====================================================================================================== 

# -sオプションをつけると、print()文が出力される
$ pytest -s test_one.py
======================================================================================================== test session starts ======================================================================================================== 
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 1 item                                                                                                                                                                                                                      

test_one.py End!
.

===================================================================================================== 1 passed in 0.02 seconds ====================================================================================================== 

他にも --capture=sysや --capture=fd などがあるが、それらはよくわからないしあんまり使わなそう。

--lf, --last-failed

$ pytest --lf

最後に失敗したテストだけが再び実行されます。失敗しているテストだけを実行するときに便利。

--ff, --failed-first

$ pytest --ff

基本的に--last-failedと同じで、最後に失敗したテストを実行した後、残りの成功しているテストを実行します。

-v, --verbose

$ pytest -v

通常よりも詳細に情報が出力されます。詳しいエラー内容を知りたいときに便利。

-q, --quit

$ pytest -q --tb=line test_two.py
F                                                                                                                                                                                                                              [100%] 
============================================================================================================= FAILURES ============================================================================================================== 
C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1\test_two.py:2: assert (1, 2, 3) == (3, 2, 1)
1 failed in 0.03 seconds

出力される情報が少なくなります。--tb=lineオプションと組み合わせることで失敗しているテストの行だけが表示されるようになります。

-l, --showlocals

失敗しているテストのローカル変数とそれらの値がトレースバックで表示されます。

$ pytest -l tasks/test_four.py
======================================================================================================== test session starts ========================================================================================================
platform win32 -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\XXXX\workspace\my-practice\PythonTestingWithPytest\ch1
collected 2 items                                                                                                                                                                                                                     

tasks\test_four.py .F                                                                                                                                                                                                          [100%]

============================================================================================================= FAILURES ============================================================================================================== 
___________________________________________________________________________________________________________ test_replace ____________________________________________________________________________________________________________ 

    @pytest.mark.run_these_please
    def test_replace():
        """_replace() should change passed in fields."""
        t_before = Task("finish book", "brian", False)
        t_after = t_before._replace(id=10, done=True)
        t_expeceted = Task("finish book", "brian", True, 11)
>       assert t_after == t_expeceted
E       AssertionError: assert Task(summary=...e=True, id=10) == Task(summary='...e=True, id=11)
E         At index 3 diff: 10 != 11
E         Use -v to get the full diff


t_after    = Task(summary='finish book', owner='brian', done=True, id=10)
t_before   = Task(summary='finish book', owner='brian', done=False, id=None) 
t_expeceted = Task(summary='finish book', owner='brian', done=True, id=11)  

tasks\test_four.py:24: AssertionError

========================================================================================== 1 failed, 1 passed, 1 warnings in 0.04 seconds =========================================================================================== 

assertが失敗したときの、t_after, t_before, t_expectedの3つの変数の中身が表示されていることがわかります。

--tb=style

$ pytest --tb=no

失敗しているテストのトレースバックを出力する方法を変更します。

--tb=shortは、assertの行とエラーが評価された行だけが表示されます。

--tb=lineは、エラーが一行にまとめられます。

--tb=noは、トレースバックが完全になくなります。

--durations=N

$ pytest --durations=3

テストが実行された後に、最も時間がかかったN個のテスト/セットアップ/ティアダウンを表示します。テスト全体を高速化するときに役に立ちます。

--durations=0の場合は、最も時間がかかったものから順に全てのテストが表示されます。

--version

$ pytest --version

pytestのバージョンとインストールされているディレクトリを表示します。

$ pytest --version
This is pytest version 5.0.1, imported from C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\pytestx\lib\site-packages\pytest.py