モーダルを閉じる工作HardwareHub ロゴ画像

工作HardwareHubは、ロボット工作や電子工作に関する情報やモノが行き交うコミュニティサイトです。さらに詳しく

利用規約プライバシーポリシー に同意したうえでログインしてください。

工作HardwareHub ロゴ画像 (Laptop端末利用時)
工作HardwareHub ロゴ画像 (Mobile端末利用時)
目次目次を開く/閉じる

pytest の基本的な使い方

モーダルを閉じる

ステッカーを選択してください

モーダルを閉じる

お支払い内容をご確認ください

購入商品
」ステッカーの表示権
メッセージ
料金
(税込)
決済方法
GooglePayマーク
決済プラットフォーム
確認事項

利用規約をご確認のうえお支払いください

※カード情報はGoogleアカウント内に保存されます。本サイトやStripeには保存されません

※記事の執筆者は購入者のユーザー名を知ることができます

※購入後のキャンセルはできません

作成日作成日
2018/05/28
最終更新最終更新
2022/08/08
記事区分記事区分
一般公開

目次

    アカウント プロフィール画像 (サイドバー)

    モバイルアプリを開発中。趣味でIoTガジェットも作っています。

    0
    ステッカーを贈るとは?

    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%
    -------------
    

    0
    詳細設定を開く/閉じる
    アカウント プロフィール画像 (本文下)

    モバイルアプリを開発中。趣味でIoTガジェットも作っています。

    記事の執筆者にステッカーを贈る

    有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。

    さらに詳しく →
    ステッカーを贈る コンセプト画像

    Feedbacks

    Feedbacks コンセプト画像

      ログインするとコメントを投稿できます。

      関連記事