pytest についてまとめる

IT

はじめに

python のサードパーティのテストフレームワークである pytest に関して、基本的な使い方をまとめます。

環境

今回使用する環境は以下です。

$ python --version
Python 3.7.10
$ pytest --version
pytest 7.1.0

使い方

実行方法

公式ドキュメント通り、基本的な使い方から確認していきます。
下記内容の test_sample.py を作成します。

# content of test_sample.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5

pytest はデフォルトでは、コード内の test で始まる名前の関数が自動的にテスト対象になります。今回は test_answer がテスト対象です。
そして各関数の中で、成立すべき式(Trueになるべき式)をassert文で記述します。

テストを実行するには、 test_sample.py が存在するディレクトリにおいて、pytest コマンドを実行します。
引数なしで実行すると、カレントディレクトリのファイル名に test_ で始まるまたは _test で終わるファイル(test_*.py or *_test.py)がテスト対象になります。

$ pytest test_sample.py
================================================================= test session starts =================================================================
platform linux -- Python 3.7.10, pytest-7.1.0, pluggy-1.0.0
rootdir: /home/ec2-user/environment/pytest
plugins: anyio-3.4.0
collected 1 item                                                                                                                                      

test_sample.py F                                                                                                                                [100%]

====================================================================== FAILURES =======================================================================
_____________________________________________________________________ test_answer _____________________________________________________________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_sample.py:6: AssertionError
=============================================================== short test summary info ===============================================================
FAILED test_sample.py::test_answer - assert 4 == 5
================================================================== 1 failed in 0.14s ==================================================================

assert 文で記載した内容が成立しないため、テストが失敗した旨のメッセージが表示されました。

test_sample.py を修正し、再度実行します。

# content of test_sample.py
def func(x):
    return x + 2

def test_answer():
    assert func(3) == 5

テストが成功した場合、下記のように、成功した旨のメッセージが表示されます。

$ pytest test_sample.py
================================================================= test session starts =================================================================
platform linux -- Python 3.7.10, pytest-7.1.0, pluggy-1.0.0
rootdir: /home/ec2-user/environment/pytest
plugins: anyio-3.4.0
collected 1 item                                                                                                                                      

test_sample.py .                                                                                                                                [100%]

================================================================== 1 passed in 0.02s ==================================================================

以上が基本的な使い方です。
以降、その他の使い方について見ていきます。

assert文の説明

assert 文の第2引数に説明文を指定することができます。
assert 文は複数記述することが可能です。

def func(x):
    return x + 2 if x % 2 == 0 else x

def test_answer():
    assert func(3) == 3, '引数が2の倍数の場合'
    assert func(4) == 6, '引数が2の倍数でない場合'

テスト項目のパラメータ化

assert 文を複数記述する場合、一つの assert 文が失敗した段階でテストが終了するため、以降のテストは実施されません。pytest.mark.parametrize デコレータを使用してパラメータ化された引数を設定することで、すべてのパラメータのテストを実行するまでテストを続行できます。

# content of test_parametrize.py
import pytest

def func(x):
    return x + 2 if x % 2 == 0 else x


@pytest.mark.parametrize(('number', 'expected'), [
    (1, 1),
    (2, 100),
    (3, 100),
    (4, 6),
])
def test_answer(number, expected):
    assert func(number) == expected

pytest を実行します。-q オプションで出力結果を簡潔にすることができます。

$ pytest -q test_parametrize.py                                                                                         
.FF.                                                                                                                                          [100%]
===================================================================== FAILURES ======================================================================
________________________________________________________________ test_is_prime[2-5] _________________________________________________________________

number = 2, expected = 5

    @pytest.mark.parametrize(('number', 'expected'), [
        (1, 1),
        (2, 5),
        (3, 10),
        (4, 6),
    ])
    def test_is_prime(number, expected):
>       assert func(number) == expected
E       assert 4 == 5
E        +  where 4 = func(2)

test_parametrize.py:14: AssertionError
________________________________________________________________ test_is_prime[3-10] ________________________________________________________________

number = 3, expected = 10

    @pytest.mark.parametrize(('number', 'expected'), [
        (1, 1),
        (2, 5),
        (3, 10),
        (4, 6),
    ])
    def test_is_prime(number, expected):
>       assert func(number) == expected
E       assert 3 == 10
E        +  where 3 = func(3)

test_parametrize.py:14: AssertionError
============================================================== short test summary info ==============================================================
FAILED test_parametrize.py::test_is_prime[2-5] - assert 4 == 5
FAILED test_parametrize.py::test_is_prime[3-10] - assert 3 == 10
2 failed, 2 passed in 0.11s

例外のテスト

特定の例外が送出されることをテストする場合は pytest.raises を使います。

# content of test_sysexit.py
import pytest

def f():
    raise SystemExit(1)

def test_mytest():
    with pytest.raises(SystemExit):
        f()

pytest を実行します。

$ pytest -q test_sysexit.py
.                                                                                                                                               [100%]
1 passed in 0.01s

クラス内の複数のメソッドのテスト

テスト関数をクラスにまとめることもできます。
クラス名は Test から始まる必要があります。

# content of test_class.py
class TestClass:
    def test_one(self):
        x = "this"
        assert "h" in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, "check")

pytest を実行します。

$ pytest -q test_class.py 
.F                                                                                                                                              [100%]
====================================================================== FAILURES =======================================================================
_________________________________________________________________ TestClass.test_two __________________________________________________________________

self = <test_class.TestClass object at 0x7fbdec9b23d0>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

test_class.py:8: AssertionError
=============================================================== short test summary info ===============================================================
FAILED test_class.py::TestClass::test_two - AssertionError: assert False
1 failed, 1 passed in 0.11s

特定のテストのみ実行

テスト対象の指定方法が様々あります。

テスト対象テストコマンド
モジュールpytest test_mod.py
ディレクトリpytest testing/.py
モジュール内の特定の関数pytest test_mod.py::test_func
特定のメソッドpytest test_mod.py::TestClass::test_method

-k オプションでテスト対象をキーワード指定することも可能です。
(How to invoke pytest)

ディレクトリ構成

ここまで、単純にするためテスト対象のアプリケーションコードとテストコードを同一のファイルに記述していましたが、当然実際の場面ではアプリケーションコードとテストコードは別ファイルにします。
公式ドキュメントでは、ベストプラクティスとして2パターンのフォルダ構成が紹介されています。

テストコードからアプリケーションードを import できるようにするため、pyproject.toml、setup.cfg、setup.py(pip のバージョン 21.3 以前を使っている場合のみ)をルートに配置し、アプリケーションコードのパッケージをインストールします。

$ pip install -e .
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[metadata]
name = PACKAGENAME

[options]
packages = find:

また pip のバージョン 21.3 以前を使っている場合は setup.py も必要になります。

# content of setup.py
from setuptools import setup

setup()

あるいは、pytest の実行コマンドを python -m pytest とすることで、pytest コマンド実行時にカレントディレクトリにパスを通すことも可能です。
その場合、pyproject.toml、setup.cfg、setup.py は不要です。

実際のアプリケーションコードとテストコードを分離する場合

次の場面で有効な構成

  • 多くのテストコードを実施する時(テストファイルが多い場合)
  • アプリケーションコードとテストコードを明確に分離したい時
pyproject.toml
setup.cfg
mypkg/
    __init__.py
    app.py
    view.py
tests/
    test_app.py
    test_view.py
    ...

テストコードをアプリケーションコードに含める場合

次の場面で有効な構成

  • アプリケーションコードとテストの関係性を分かりやすくしたい時
  • テストコードも含めてアプリケーションコードを配布したい時
pyproject.toml
setup.cfg
mypkg/
    __init__.py
    app.py
    view.py
    test/
        __init__.py
        test_app.py
        test_view.py
        ...

おわりに

本記事では python のテストフレームワークである pytest の基本的な使い方についてまとめました。テストフレームワークを利用することでテストの自動化に繋げることができるので、使えるようにしておきたいですね。この記事がどなたかの参考になれば幸いです。

参考

コメント