CLOVER🍀

That was when it all began.

pytestを䜿っお、Pythonのテストコヌドを曞く

これは、なにをしたくお曞いたもの

Pythonでテストコヌドを曞く時には、pytestを䜿うのが良いずいう話を聞きたしお。1床、自分でも抌さえおおこうかな、ず。

https://docs.pytest.org/en/latest/

以前、unittestで蚘茉した内容のpytest版です。

unittestライブラリで、Pythonのテストコードを書いて実行する - CLOVER🍀

pytestずは

いわゆる、テスティングフレヌムワヌクです。

以䞋が特城だそうです。

  • アサヌションに倱敗した時の詳现情報衚瀺
  • テストモゞュヌルおよび関数のオヌトディスカバリヌ
  • 小さなテストや、パラメタラむズ化された長期間のテストのためのテストリ゜ヌスを管理する、モゞュヌル化されたfixture
  • unittestおよびnoseのテストケヌスを実行可胜
  • Python 3.5以䞊、PyPy3䞊で動䜜
  • リッチなプラグむンアヌキテクチャによる、315以䞊の倖郚プラグむンずコミュニティ

䞋䜍互換性や、Python 2.7および3.4に関するサポヌトに぀いおは、こちら。

Backwards Compatibility Policy — pytest documentation

Python 2.7 and 3.4 support — pytest documentation

APIドキュメントに぀いおは、こちらです。

API Reference — pytest documentation

たあ、ずりあえず䜿っおいっおみたしょう。

環境

今回の環境は、こちらです。

$ python3 -V
Python 3.6.9


$ pip3 -V
pip 9.0.1 from /path/to/venv/lib/python3.6/site-packages (python 3.6)

むンストヌル

たずは、むンストヌルしたしょう。

Installation and Getting Started — pytest documentation

pip3でむンストヌル。

$ pip3 install pytest

今回䜿甚するpytestのバヌゞョンは、5.3.5です。

$ pip3 freeze | grep pytest
pytest==5.3.5

ちなみに、このペヌゞにサポヌトするPythonのバヌゞョンずプラットフォヌムがハッキリず曞かれおいたすね。

この環境䞊に、テスト察象およびテストコヌドを曞いおいきたしょう。

お題ずプロゞェクト構成

お題ずプロゞェクト構成も、基本的にはunittestの時に曞いたものに合わせたす。

unittestライブラリで、Pythonのテストコードを書いて実行する - CLOVER🍀

たずはディレクトリレむアりト。

Choosing a test layout / import rules

テストコヌドをテスト察象アプリケヌションコヌド偎に眮くスタむルず、そうでないスタむルがあるようですが、今回は
アプリケヌションコヌドずテストコヌドをハッキリ分けお眮きたいず思いたす。

Tests outside application code

アプリケヌションコヌドを眮くディレクトリを「sample」、テストコヌドを眮くディレクトリを「tests」ずしたす。

$ mkdir sample tests

アプリケヌションコヌド偎を、1床「src」ディレクトリに眮くずいう話もあるようですが、これはたた別の機䌚に芋おみたしょう。

Packaging a python library | ionel's codelog

アプリケヌションコヌドおよび、テストを䜜成しお実行しおみる

では、たずアプリケヌションコヌドを䜜成。
sample/calc.py

class Calc:
    def add(self, x, y):
        return x + y

    def minus(self, x, y):
        return x - y

    def multiply(self, x, y):
        return x * y

    def divide(self, x, y):
        return x / y

続いお、テストコヌドを曞いおみたす。
tests/test_calc.py

from sample.calc import Calc

def test_add():
    calc = Calc()
    assert calc.add(1, 3) == 4

def test_minus():
    calc = Calc()
    assert calc.minus(5, 3) == 2

def test_multiply():
    calc = Calc()
    assert calc.multiply(2, 3) == 6

def test_divide():
    calc = Calc()
    assert calc.divide(10, 2) == 5

pytestがどのようにテストコヌドを怜出するかは、こちらに蚘茉がありたす。

Conventions for Python test discovery

「test*.py」たたは「*test.py」ファむルを探玢し、関数名に「test」が接頭蟞ずしお付䞎された関数およびクラス名に「Test」が
接頭蟞ずしお付䞎されたテスト関数を察象ずするようです。

芁するに、「test」クラスの堎合は「Test」を付けおください、ず。

たた、テストコヌドを認識しおもらうには「init.py」が必芁なようなので

Choosing a test layout / import rules

You can use Python3 namespace packages (PEP420) for your application but pytest will still perform test package name discovery based on the presence of init.py files.

䜜成しおおきたす。

$ touch tests/__init__.py

では、pytestを実行しおみたす。

たずはpytestコマンドを匕数なしで実行。

$ pytest
========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                        

tests/test_calc.py ....                                                                                                                                            [100%]

=========================================================================== 4 passed in 0.01s ============================================================================

テストコヌドを探しおきお、実行しおくれたした。

テストコヌドを指定。

$ pytest tests/test_calc.py 
========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                        

tests/test_calc.py ....                                                                                                                                            [100%]

=========================================================================== 4 passed in 0.01s ============================================================================

「-v」オプションを付けお実行するず、もっず詳现に情報を出力しおくれたす。

$ pytest -v
========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /path/to/venv/bin/python3
cachedir: .pytest_cache
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                        

tests/test_calc.py::test_add PASSED                                                                                                                                [ 25%]
tests/test_calc.py::test_minus PASSED                                                                                                                              [ 50%]
tests/test_calc.py::test_multiply PASSED                                                                                                                           [ 75%]
tests/test_calc.py::test_divide PASSED                                                                                                                             [100%]

=========================================================================== 4 passed in 0.01s ============================================================================

ちょっず、テストを1床倱敗させおみたしょう。

テストコヌドをこのように倉曎しお

def test_add():
    calc = Calc()
    assert calc.add(1, 2) == 4
#    assert calc.add(1, 3) == 4

実行。

$ pytest
=========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                         

tests/test_calc.py F...                                                                                                                                             [100%]

================================================================================ FAILURES =================================================================================
________________________________________________________________________________ test_add _________________________________________________________________________________

    def test_add():
        calc = Calc()
>       assert calc.add(1, 2) == 4
E       assert 3 == 4
E        +  where 3 = <bound method Calc.add of <sample.calc.Calc object at 0x7febb711b128>>(1, 2)
E        +    where <bound method Calc.add of <sample.calc.Calc object at 0x7febb711b128>> = <sample.calc.Calc object at 0x7febb711b128>.add

tests/test_calc.py:5: AssertionError
======================================================================= 1 failed, 3 passed in 0.03s =======================================================================

どういう状態で倱敗したかがわかるんですね。

>       assert calc.add(1, 2) == 4
E       assert 3 == 4
E        +  where 3 = <bound method Calc.add of <sample.calc.Calc object at 0x7febb711b128>>(1, 2)
E        +    where <bound method Calc.add of <sample.calc.Calc object at 0x7febb711b128>> = <sample.calc.Calc object at 0x7febb711b128>.add

The writing and reporting of assertions in tests — pytest documentation

䟋倖をアサヌションしたい堎合は、こちら。

Assertions about expected exceptions

確認できたので、テストコヌドは倱敗しないように戻しおおきたす。

前凊理・埌凊理を入れる

よくある、テストの実行前埌に前凊理・埌凊理を入れおみたいず思いたす。

Fixtureを䜿うみたいです。

Fixture finalization / executing teardown code

先ほど䜜ったテストコヌドをベヌスにしお

$ cp tests/test_calc.py tests/test_calc_seup_teardown.py

こんな感じに䜜成。
tests/test_calc_seup_teardown.py

from sample.calc import Calc

import pytest

@pytest.fixture
def setup_and_teardown():
    print("Setup.")
    yield
    print("Teardown.")

def test_add(setup_and_teardown):
    calc = Calc()
    print("call add")
    assert calc.add(1, 3) == 4

def test_minus(setup_and_teardown):
    calc = Calc()
    print("call minus")
    assert calc.minus(5, 3) == 2

def test_multiply():
    calc = Calc()
    print("call multiply")
    assert calc.multiply(2, 3) == 6

def test_divide():
    calc = Calc()
    print("call divide")
    assert calc.divide(10, 2) == 5

@pytest.fixtureでデコレヌトした関数を定矩しお

@pytest.fixture
def setup_and_teardown():
    print("Setup.")
    yield
    print("Teardown.")

前埌凊理を挟み蟌みたいテスト関数の匕数に蚭定したす。

def test_add(setup_and_teardown):
    calc = Calc()
    print("call add")
    assert calc.add(1, 3) == 4

def test_minus(setup_and_teardown):
    calc = Calc()
    print("call minus")
    assert calc.minus(5, 3) == 2

確認。

$ pytest tests/test_calc_seup_teardown.py 
=========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                         

tests/test_calc_seup_teardown.py ....                                                                                                                               [100%]

============================================================================ 4 passed in 0.01s ============================================================================

どうなったか、たったくわかりたせん。

これは、今回printでデバッグ的に確認しようずしおいるのですが、stdinstdoutがpytestでキャプチャされおいるからです。

Capturing of the stdout/stderr output — pytest documentation

今回の確認目的では、キャプチャをオフにすればprintの様子を芳るこずができたす。

$ pytest --capture=no tests/test_calc_seup_teardown.py

## もしくは
$ pytest -s tests/test_calc_seup_teardown.py
=========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                         

tests/test_calc_seup_teardown.py Setup.
call add
.Teardown.
Setup.
call minus
.Teardown.
call multiply
.call divide
.

============================================================================ 4 passed in 0.01s ============================================================================

テスト関数の匕数にFixtureを蚭定した、add、minusに関しおはSetupずTeardownのメッセヌゞが芋えおいたすね。

ちなみに、キャプチャをオフにしなくおもテストに倱敗した堎合は

def test_add(setup_and_teardown):
    calc = Calc()
    print("call add")
    assert calc.add(1, 4) == 4

その関数でキャプチャされた情報などが出力されたす。

$ pytest tests/test_calc_seup_teardown.py 
=========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                         

tests/test_calc_seup_teardown.py F...                                                                                                                               [100%]

================================================================================ FAILURES =================================================================================
________________________________________________________________________________ test_add _________________________________________________________________________________

setup_and_teardown = None

    def test_add(setup_and_teardown):
        calc = Calc()
        print("call add")
>       assert calc.add(1, 4) == 4
E       assert 5 == 4
E        +  where 5 = <bound method Calc.add of <sample.calc.Calc object at 0x7fddc8d5fcc0>>(1, 4)
E        +    where <bound method Calc.add of <sample.calc.Calc object at 0x7fddc8d5fcc0>> = <sample.calc.Calc object at 0x7fddc8d5fcc0>.add

tests/test_calc_seup_teardown.py:14: AssertionError
-------------------------------------------------------------------------- Captured stdout setup --------------------------------------------------------------------------
Setup.
-------------------------------------------------------------------------- Captured stdout call ---------------------------------------------------------------------------
call add
------------------------------------------------------------------------ Captured stdout teardown -------------------------------------------------------------------------
Teardown.
======================================================================= 1 failed, 3 passed in 0.03s =======================================================================

たた、Fixtureを䜿うこずでテスト関数の匕数を蚭定したり、テスト間でデヌタを共有できたりしたす。

Fixtures as Function arguments

Sharing test data

このあたりも、芚えおおきたしょう。

たずめ

Pythonのテスティングフレヌムワヌク、pytestを詊しおみたした。

ずりあえず、初歩的なこずは少しは抌さえられたのではないでしょうか。これから、䜿っおいっおみたしょう。