目次
モバイルアプリを開発中。趣味でIoTガジェットも作っています。
pytest の基本的な使い方を記載します。
適宜参照するための公式ドキュメントページ
インストール
適当なパッケージマネージャ等でインストールできます。
sudo apt install python-pip
pip install pytest
which pytest
/home/vagrant/.local/bin/pytest
実行方法
test_sample.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
class TestClass(object):
def test_one(self):
x = "this"
assert 'h' in x
def test_two(self):
self.y = "hello"
assert hasattr(self, 'y')
ファイル指定
pytest test_sample.py
出力内容を最小限にして実行
pytest -q test_sample.py
複数ファイル
カレントディレクトリ以下の test_*.py
または *_test.py
という形式のファイルを再帰的にすべて実行します。
pytest
特定のテストを指定
pytest test_sample.py::TestClass::test_two
Python コードから pytest を実行
pytest.main()
を利用します。
test_sample.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
from pytest import main
class TestClass(object):
def test_one(self):
x = "this"
assert 'h' in x
def test_two(self):
self.y = "hello"
assert hasattr(self, 'y')
if __name__ == '__main__':
sys.exit(main())
python コマンドを実行
python test_sample.py
python test_sample.py -k test_one
decorator のあるテストのみを実行
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
def func(x):
return x + 1
def test_one():
assert func(4) == 5
@pytest.mark.myslowmark
def test_two():
assert func(4) == 5
二つ目のみ実行されます
pytest -m myslowmark
最初に失敗したら停止
pytest -x
テスト失敗時にデバッガを起動
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
def func(x):
return x + 1
def test_one():
assert func(4) == 0
x = 123
def test_two():
assert func(4) == 5
-x
と組み合わせて、テスト失敗時に pdb に入るようにできます。
pytest -x --pdb
サンプルコード
値の検証、例外の検証
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
def f():
return 3
# 値の検証
def test_assert():
assert f() == 3
# 例外の検証
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
fixtures の利用
テストに必要なデータを fixture として設定して利用できます。
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
@pytest.fixture
def f():
return 1
@pytest.fixture
def g(f):
return 10 + f
def test_one(g):
assert g == 11
利用可能な fixture は以下のコマンドで確認できます。
$ pytest --fixtures test_sample.py
...
------- fixtures defined from test_sample -----
g
test_sample.py:11: no docstring available
f
test_sample.py:7: no docstring available
fixture は既定ではテスト関数毎のスコープを持ちますが、module
や session
に変更することで、テスト間での fixture の共有ができます。作成に時間がかかる fixture の場合はテスト時間の短縮にもなります。pytest は conftest.py という名称のファイルを自動的に読み込むため、ここに共有の fixture を設定できます。
conftest.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
@pytest.fixture(scope = "module")
def ff():
return 1
@pytest.fixture(scope = "session")
def gg():
return 1
test_sample.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
def test_one(ff):
assert ff == 1
パラメータの利用
import pytest
@pytest.mark.parametrize("xxx, yyy", [
(1, 3),
(2, 4),
])
def test_one(xxx, yyy):
assert xxx < yyy
一時ディレクトリの利用
import pytest
def test_create_file(tmpdir):
p = tmpdir.mkdir("sub").join("hello.txt")
p.write("content")
assert p.read() == "content"
生成された一時ディレクトリ
ls /tmp/pytest-of-vagrant/
pytest-2 pytest-3 pytest-4 pytest-5 pytest-vagrant
テストの skip
DB などの外部リソースに依存している等の理由で、条件によって省略したいテストを設定できます。
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
@pytest.mark.skip(reason="xxx")
def test_one():
pass
def test_two():
if True:
pytest.skip("xxx")
pass
@pytest.mark.skipif(True, reason="xxx")
def test_three():
pass
mymark = pytest.mark.skipif(True, reason="yyy")
@mymark
def test_four():
pass
テスト時のログ設定
pytest.ini
[pytest]
log_cli = 1
log_cli_level = DEBUG
log_cli_date_format = %Y-%m-%d %H:%M:%S
log_cli_format = %(asctime)s %(levelname)s %(message)s
log_file = pytest.log
log_file_level = DEBUG
log_file_date_format = %Y-%m-%d %H:%M:%S
log_file_format = %(asctime)s %(levelname)s %(message)s
test_sample.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
import logging
logger = logging.getLogger(__name__)
def test_one():
logger.info("xxx")
logger.info("xxx")
logger.info("xxx")
logger.info("xxx")
logger.info("xxx")
logger.info("xxx")
print('hi')
pass
実行例
pytest test_sample.py
=============================================================================================== test session starts ===============================================================================================
platform linux2 -- Python 2.7.16, pytest-4.6.5, py-1.8.0, pluggy-0.13.0
rootdir: /home/username/test_sample, inifile: pytest.ini
collected 1 item
test_sample.py::test_one
-------------------------------------------------------------------------------------------------- live log call --------------------------------------------------------------------------------------------------
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
PASSED
cat pytest.log
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
2019-09-18 01:17:12 INFO xxx
pytest オプション -s
を利用するとテストケース内の標準出力 -s
も出力されます。
2019-09-18 01:17:00 INFO xxx
hi
PASSED
アサーションをカスタマイズする
失敗時の時刻を出力する例です。
pytest.ini
[pytest]
log_cli = 1
log_cli_level = DEBUG
log_cli_date_format = %Y-%m-%d %H:%M:%S
log_cli_format = %(asctime)s %(levelname)s %(message)s
test_sample.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
from helpers import myassert
def test_one():
myassert(1 == 1)
helpers/__init__.py
# -*- coding: utf-8 -*-
import pytest
pytest.register_assert_rewrite('helpers.myassert')
from .myassert import myassert
helpers/myassert.py
# -*- coding: utf-8 -*-
import logging
logger = logging.getLogger(__name__)
def myassert(res):
try:
assert res
except AssertionError:
logger.error('ASSERTION FAILED')
raise
プラグイン
pytest-repeat
テストケースを複数回実行します。以下の例では各テストが二回実行されます。
pip install pytest-repeat
pytest test_sample.py --count 2
pytest-forked
テストケースを別プロセスで実行します。fixture の scope が class や module であっても状態が引き継がれなくなります。
pip install pytest-forked
test_sample.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
class TestClass(object):
@pytest.fixture(scope = "class")
def myfixture(self):
data = []
return data
def test_one(self, myfixture):
assert len(myfixture) == 0
myfixture.append(123)
def test_two(self, myfixture):
assert len(myfixture) == 1
forked を付与すると失敗することが分かります。
pytest test_sample.py -q
.. [100%]
2 passed in 0.01 seconds
pytest test_sample.py -q --forked
.F
===================================== FAILURES ======================================
________________________________ TestClass.test_two _________________________________
self = <test_sample.TestClass object at 0x7f3aab067c10>, myfixture = []
def test_two(self, myfixture):
> assert len(myfixture) == 1
E assert 0 == 1
E + where 0 = len([])
test_sample.py:18: AssertionError
1 failed, 1 passed in 0.07 seconds
pytest-ordering
pip install pytest-ordering
指定した順番に実行されます。
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pytest
@pytest.mark.run(order=-2)
def test_three():
assert True
@pytest.mark.run(order=-1)
def test_four():
assert True
@pytest.mark.run(order=2)
def test_two():
assert True
@pytest.mark.run(order=1)
def test_one():
assert True
実行結果例
$ pytest -v sample.py
sample.py::test_one PASSED
sample.py::test_two PASSED
sample.py::test_three PASSED
sample.py::test_four PASSED
pytest-timeout
pip install pytest-timeout
テストの実行に時間制限を設定できます。
$ pytest sample.py
@pytest.mark.timeout(5)
def test_foo():
> time.sleep(10)
E Failed: Timeout >5.0s
sample.py:9: Failed
pytest-cov
pip install pytest-cov
テストコードによる、テスト対象のコードパスの網羅率を計測できます。
python/utils.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
def Compare(a, b):
if a > b:
return 1
elif a < b:
return -1
else:
return 0
test/test_sample.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
from utils import Compare
def test_sample():
assert Compare(1, 2) is -1
assert Compare(2, 1) is 1
# assert Compare(0, 0) is 0
実行例
$ PYTHONPATH=python:$PYTHONPATH pytest --cov=python test/test_sample.py
---------- coverage: platform linux2, python 2.7.16-final-0 ----------
Name Stmts Miss Cover
-------------------------------------
python/utils.py 6 1 83%
==== 1 passed in 0.08 seconds =======
--cov-report=html
オプションで htmlcov/*
を生成することもできます。
diff-cover
あるブランチのマージについて、その差分に関するテストの網羅率を計測できます。pytest-cov が出力する情報と git の差分を利用します。
pip install diff_cover
あるブランチの coverage.xml
を生成
PYTHONPATH=python:$PYTHONPATH pytest --cov=python --cov-report=xml test/test_sample.py
マージ対象として master を指定することで、master に対する変更箇所のテストカバレッジを計測できます。
$ git branch
master
* mybranch
変更箇所
$ git diff master python/
diff --git a/python/utils.py b/python/utils.py
+def Compare2(a, b):
+ if a > b:
+ return 1
+ elif a < b:
+ return -1
+ else:
+ return 0
テストのカバー率
$ diff-cover coverage.xml --compare-branch master --html-report diff-cover.html
-------------
Diff Coverage
Diff: master...HEAD, staged and unstaged changes
-------------
python/utils.py (16.7%): Missing lines 13-16,18
-------------
Total: 6 lines
Missing: 5 lines
Coverage: 16%
-------------
記事の執筆者にステッカーを贈る
有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。
さらに詳しく →Feedbacks
ログインするとコメントを投稿できます。
関連記事
- Python コードスニペット (条件分岐)if-elif-else sample.py #!/usr/bin/python # -*- coding: utf-8 -*- # コメント内であっても、ASCII外の文字が含まれる場合はエンコーディング情報が必須 x = 1 # 一行スタイル if x==0: print 'a' # 参考: and,or,notが使用可能 (&&,||はエラー) elif x==1: p...
- Python コードスニペット (リスト、タプル、ディクショナリ)リスト range 「0から10まで」といった範囲をリスト形式で生成します。 sample.py print range(10) # for(int i=0; i<10; ++i) ← C言語などのfor文と比較 print range(5,10) # for(int i=5; i<10; ++i) print range(5,10,2) # for(int i=5; i<10;...
- ZeroMQ (zmq) の Python サンプルコードZeroMQ を Python から利用する場合のサンプルコードを記載します。 Fixing the World To fix the world, we needed to do two things. One, to solve the general problem of "how to connect any code to any code, anywhere". Two, to wra...
- Matplotlib/SciPy/pandas/NumPy サンプルコードPython で数学的なことを試すときに利用される Matplotlib/SciPy/pandas/NumPy についてサンプルコードを記載します。 Matplotlib SciPy pandas [NumPy](https://www.numpy
- PID 制御による全方向移動ロボットの位置制御 (ODE、Python)Open Dynamics Engine (ODE) を用いて、全方向移動ロボットの位置制御を PID 制御で行う場合のサンプルを記載します。差分駆動型ロボットと比較して、全方向移動ロボットは任意の方向に移動できるため位置制御が容易です。 モータの角速度を操作することでロボットの位置を制御 目標値 xdx_dxd と現在時刻における測定値 x(t)x(t)x(t) の残差 e(t)e(t)e(t...