pytest の基本的な使い方
[History] [Last Modified] (2020/04/03 13:45:57)
ここは
趣味のプログラミングを楽しむための情報共有サービス。記事の一部は有料設定にして公開できます。 詳しくはこちらをクリック📝
Recent posts
Popular pages

概要

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 は既定ではテスト関数毎のスコープを持ちますが、modulesession に変更することで、テスト間での 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%
-------------

Uploaded Image

Related pages