これは、なにをしたくて書いたもの?
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
https://docs.pytest.org/en/latest/py27-py34-deprecation.html
APIドキュメントについては、こちらです。
https://docs.pytest.org/en/5.3.5/reference.html
まあ、とりあえず使っていってみましょう。
環境
今回の環境は、こちらです。
$ python3 -V Python 3.6.9 $ pip3 -V pip 9.0.1 from /path/to/venv/lib/python3.6/site-packages (python 3.6)
インストール
まずは、インストールしましょう。
https://docs.pytest.org/en/5.3.5/getting-started.html
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
https://docs.pytest.org/en/5.3.5/assert.html
例外をアサーションしたい場合は、こちら。
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でデバッグ的に確認しようとしているのですが、標準入力/標準出力がpytestでキャプチャされているからです。
https://docs.pytest.org/en/5.3.5/capture.html
今回の確認目的では、キャプチャをオフにすれば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
このあたりも、覚えておきましょう。
まとめ
Pythonのテスティングフレームワーク、pytestを試してみました。
とりあえず、初歩的なことは少しは押さえられたのではないでしょうか。これから、使っていってみましょう。