shimada-kの日記

ソフトウェア・エンジニアのブログです

pytestのfixtureについて

この記事は「LIFULL Advent Calendar 2018」5日目の記事として書かれました。

f:id:shimada-k:20181205201906p:plain

qiita.com

今年もアドベントカレンダーの季節になりました。年々ブログを書くという行為から遠ざかっている気がします。勤務先の会社がLIFULLグループではなくなる予定なので「LIFULL Advent Calendar」の参加も今年が最後になります。来年は組織のアドベントカレンダーではなく特定の技術のものに参加することになるでしょう。

仕事でpytestを使ってテストコードを書いているのでpytestの便利な機能について紹介したいと思います。

はじめに

Python標準のunittestや他のテストフレームワークとの大きな違いはfixtureが強力というところだとと思います。unittestではsetUpClassやtearDownClassを使用することになります。

pytestではもともとfuncargという仕組みがあり、テストコード共通で使う変数を定義することができました。Pytest-2.3からより汎用的な仕組みとして fixtureが導入されました。

fixtureは高機能でテストケースで使用する変数を定義すること以外にもマニュアルに目を通すと他にも柔軟な使い方ができそうなのでいくつか取り上げたいと思います。

pytest-fixture

https://docs.pytest.org/en/latest/fixture.htmldocs.pytest.org

pytestのfixtureは依存性の注入です。セッション、モジュール、クラス、ファンクションとスコープを設定できます。 本家のドキュメントを見ているといくつか有用そうな使用について紹介します。ソースは全て引用です。

ファクトリパターン

@pytest.fixture
def make_customer_record():

    def _make_customer_record(name):
        return {
            "name": name,
            "orders": []
        }

    return _make_customer_record


def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")

fixtureを複数定義するほどではないが、共通部分が多いという場合に使用できそうです。

複数パラメータ

# content of conftest.py
import pytest
import smtplib

@pytest.fixture(scope="module",
                params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
    smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
    yield smtp_connection
    print("finalizing %s" % smtp_connection)
    smtp_connection.close()

parametrizeライブラリと似た様な感じですが、parametrizeはfixtureとは別の仕組みとして提供されるので、conftest.pyに書きたいならfixtureのparamsを、テストコード側で定義したいならparametrizeといった使い分けかなと思います。

fixture内でのオブジェクト返却

# content of test_appsetup.py

import pytest

class App(object):
    def __init__(self, smtp_connection):
        self.smtp_connection = smtp_connection

@pytest.fixture(scope="module")
def app(smtp_connection):
    return App(smtp_connection)

def test_smtp_connection_exists(app):
    assert app.smtp_connection

フィックスチャとして利用したいものが単純な変数やデータではなくオブジェクトになる場合はこの様な使用になると思います。ファクトリパターンのもっと構造化されたものという感じでしょうか。

テストケース内のパラメータ読み取り

# content of test_anothersmtp.py

smtpserver = "mail.python.org"  # will be read by smtp fixture

def test_showhelo(smtp_connection):
    assert 0, smtp_connection.helo()
# content of conftest.py
import pytest
import smtplib

@pytest.fixture(scope="module")
def smtp_connection(request):
    server = getattr(request.module, "smtpserver", "smtp.gmail.com")
    smtp_connection = smtplib.SMTP(server, 587, timeout=5)
    yield smtp_connection
    print("finalizing %s (%s)" % (smtp_connection, server))
    smtp_connection.close()

テストケース内でフィックスチャに渡すデータを定義するケースはぱっと思いつきませんが、fixture内でオブジェクトを返却するケースで、クラスを統一したいというケースかなと思います。「fixture内でのオブジェクト返却」と「ファクトリパターン」の組み合わせになるかと思います。

まとめ

Pytestはfixtureが充実しているのでsetup/teardownをモジュールごとに書く必要がなくなっています。そのことでテストコードの可読性が高いです。

またディレクトリごとにconftest.pyを配置することでシンプルながらも、オーバーライドも可能となっており柔軟性も高いです。

fixture以外にも実は知っていればもっと効率的にテストコードを書けるということがあると思うので、一度マニュアルに目を通すと発見があると思います。