search
LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

Organization

pytestのフィクスチャが便利だった

はじめに

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*

エラーメッセージがわかりやすい

ここでのテストは pytestpython の文字列を比較する簡易的なものを使います。

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

成功したテストの.前後で setupteardown が表示されていることが確認できました。

※ 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_namelast_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を試してもらえると嬉しいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2