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
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
#!/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
テストに必要なデータを 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
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
テストケースを複数回実行します。以下の例では各テストが二回実行されます。
pip install pytest-repeat
pytest test_sample.py --count 2
テストケースを別プロセスで実行します。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
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
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
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/*
を生成することもできます。
あるブランチのマージについて、その差分に関するテストの網羅率を計測できます。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%
-------------