はじめに
2020年度XTechグループアドベントカレンダーの18日目の記事です。
XTech株式会社のタカクです。
ここ最近Pythonを使って開発をしています。
Pythonには標準のテストフレームワークであるunittestがありますが、私が関わっているプロジェクトでは3rdパーティー製のpytestを好んで使っています。
pytestはテスト失敗時に詳細なメッセージを表示してくれたり、フィクスチャの機能が便利なことから採用しています。
今回はpytestの基本とフィクスチャについて書きます。
pytestの基本
pytestは3rdパーティー製のテストフレームワークのため、Python標準のunittetとは違い個別でのインストールが必要なのでインストール。
$ pip install pytest
unittestはクラスベースなのに対してpytestは関数ベースです。
assertするにもunittestではself.assertですが、pytestではassertでできます。
def test_calc():
assert 1 + 1 == 2
実行結果。テストが成功した場合は.が付きます。
$ pytest
============================================ test session starts ============================================
platform darwin -- Python 3.7.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/*/test
collected 1 item
calc.py . [100%]
============================================= 1 passed in 0.01s =============================================
pytestが対象とするテストの命名規則
- ファイル名: test_*.py または *_test.py
- テストメソッドやテスト関数名はtest_*
- テストクラスの名前はTest*
エラーメッセージがわかりやすい
ここでのテストは pytest
と python
の文字列を比較する簡易的なものを使います。
def test_pytest():
assert ('p', 'y', 't', 'e', 's', 't') == ('p', 'y', 't', 'h', 'o', 'n')
-v/--verbose
オプションを付けると詳細なエラーメッセージを表示してくれますが、まずはオプションをつけづにエラーメッセージを表示させます。
$ pytest
============================================ test session starts ============================================
platform darwin -- Python 3.7.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/*/test
collected 1 item
test_pytest.py F [100%]
================================================= FAILURES ==================================================
________________________________________________ test_pytest ________________________________________________
def test_pytest():
> assert ('p', 'y', 't', 'e', 's', 't') == ('p', 'y', 't', 'h', 'o', 'n')
E AssertionError: assert ('p', 'y', 't', 'e', 's', 't') == ('p', 'y', 't', 'h', 'o', 'n')
E At index 3 diff: 'e' != 'h'
E Use -v to get the full diff
test_pytest.py:2: AssertionError
========================================== short test summary info ==========================================
FAILED test_pytest.py::test_pytest - AssertionError: assert ('p', 'y', 't', 'e', 's', 't') == ('p', 'y', 't', 'h', '...
============================================= 1 failed in 0.08s =============================================
3番目のインデックスのエラーを指摘してくれていますね。他にもあるけどとりあえず最初に一つのみ。
続いて Use -v to get the full diff
と書いてあるように -v
オプション付けて実行してみます。
$ pytest -v
================================================= test session starts ==================================================
platform darwin -- Python 3.7.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 -- /Users/*/.pyenv/versions/3.7.2/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/*/test
collected 1 item
test_pytest.py::test_pytest FAILED [100%]
======================================================= FAILURES =======================================================
_____________________________________________________ test_pytest ______________________________________________________
def test_pytest():
> assert ('p', 'y', 't', 'e', 's', 't') == ('p', 'y', 't', 'h', 'o', 'n')
E AssertionError: assert ('p', 'y', 't', 'e', 's', 't') == ('p', 'y', 't', 'h', 'o', 'n')
E At index 3 diff: 'e' != 'h'
E Full diff:
E - ('p', 'y', 't', 'h', 'o', 'n')
E ? ^ ^ ^
E + ('p', 'y', 't', 'e', 's', 't')
E ? ^ ^ ^
test_pytest.py:2: AssertionError
=============================================== short test summary info ================================================
FAILED d.py::test_pytest - AssertionError: assert ('p', 'y', 't', 'e', 's', 't') == ('p', 'y', 't', 'h', 'o', 'n')
================================================== 1 failed in 0.05s ===================================================
今回は Full diffのところに3箇所のエラーがしっかりと指摘されてます!
どこがダメなのか明確にしてくれるのでテストが捗ります。
フィクスチャ
pytestのフィクスチャはテストの前処理と後処理をする機能を持っています。
unittestのsetupとteardownとは違いテスト関数から分離することができます。
無駄な記述がなく、シンプルになるのでテストの可読性が上がるのがいいですね。
フィクスチャ関数
@pytest.fixture()
修飾子をつけるとpytestがフィクスチャ関数を認識してくれます。
パラメータとしてフィクスチャ関数名をテストの関数に渡すことができる。
import pytest
@pytest.fixture()
def param():
return ('p', 'y', 't', 'e', 's', 't')
def test_pytest(param):
assert param == ('p', 'y', 't', 'e', 's', 't')
フィクスチャを共有する
conftest.pyファイルに記述することで再利用可能なフィクスチャになる。
pytestは現在のテストモジュール、conftest.pyの順番にフィクスチャを検索している。
pytestが探してくれるのでconftestはimportしなくて使える。
conftest.pyはプロジェクトのテストディレクトリルートと必要に応じて各テストディレクトリ配下に置けます。
conftest.pyに今まで同様にフィクスチャ関数を定義するだけです。
import pytest
@pytest.fixture()
def param():
return ('p', 'y', 't', 'e', 's', 't')
フィクスチャでセットアップとティアダウンをする
フィクスチャではフィクスチャ関数内でyieldを使うことでsetupとteardownを実現できます
フィクスチャ関数内にyieldがあると先にtest関数が実行され、test関数の実行が終わってからフィクスチャ関数に制御が戻り、yield以降の処理が実行されます。
@pytest.fixture()
def setup_and_teardown():
print('\nsetup')
yield
print('\nteardown')
テスト(yield)の前後で実行されているか確認する。
Mac:test$ pytest --capture=no
================================================= test session starts ==================================================
platform darwin -- Python 3.7.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/*/test
collected 1 item
setup.py
setup
.
teardown
================================================== 1 passed in 0.01s ===================================================
成功したテストの.前後で setup
とteardown
が表示されていることが確認できました。
※ printの出力はERRORの時にしか表示されないので、ここでは --capture=no
オプションをつけています。
フィクスチャのパラメータ化
@pytest.fixtureのパラメータparamsを使うことでフィクスチャをパラメータ化することができます。
パラメータのparamsにデータを渡すことでフィクスチャ関数のparamにリストが1件づつ代入されます。
テスト関数の引数にパラメータ化されたフィクスチャを渡すことでその件数分テストが実行される。
import pytest
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return '%s %s' % (self.first_name, self.last_name)
@pytest.fixture(params=(Person('Ichiro', 'Suzuki'), Person('Taro', 'Yamamoto'),))
def person(request):
return request.param
def test(person):
expected = '%s %s' % (person.first_name, person.last_name)
assert person.full_name == expected
パラメータ化のテストを実行します。
$ pytest -v
================================================= test session starts ==================================================
platform darwin -- Python 3.7.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 -- /Users/*/.pyenv/versions/3.7.2/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/*/test
collected 2 items
person.py::test[person0] PASSED [ 50%]
person.py::test[person1] PASSED [100%]
================================================== 2 passed in 0.01s ===================================================
フィクスチャをパラメータ化したテストが正しく実行できました。
ただ、テスト結果からテストが実行できたことはわかりますが、どのようなテストなのかもう少しわかりやすくすることができます。
@pytest.fixture
関数のids
パラメータを使うと実行結果ページのパラメータの内容をカスタマイズできます。
import pytest
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return f'{self.first_name} {self.last_name}'
def ids(value):
return f'Person({value.first_name} {value.last_name})'
@pytest.fixture(params=(Person('Ichiro', 'Suzuki'), Person('Taro', 'Yamamoto'),), ids=ids)
def person(request):
return request.param
def test(person):
expected = f'{person.first_name} {person.last_name}'
assert person.full_name == expected
ここではPersonオブジェクトの first_name
と last_name
を表示するようにしました。
$ pytest -v
================================================= test session starts ==================================================
platform darwin -- Python 3.7.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 -- /Users/*/.pyenv/versions/3.7.2/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/*/test
collected 2 items
person.py::test[Person(Ichiro Suzuki)] PASSED [ 50%]
person.py::test[Person(Taro Yamamoto)] PASSED [100%]
================================================== 2 passed in 0.01s ===================================================
person.py::test[person0]
から person.py::test[Person(Ichiro Suzuki)]
になりテストの内容がわかりますね。
まとめ
pytest
- pytestは3rdパーティー製なのでインストール必要
- 関数ベースでassertだけで使える
- ファイル名、関数名に命名規則がある
fixture
- fixture関数をテスト関数に渡せる
- fixtureをconftest.py使うことで共有できる
- yield使うことでsetup/tearDown的なことができる
- fixtureのパラメータ化ができる
pytestのフィクスチャにはここで紹介した以外にも色々な機能があります。
Pythonでテストを書く機会があったらpytestを試してもらえると嬉しいです。