pytestはPythonのテストフレームワークの一つ。
unittestなど他のフレームワークと比較して、テストに失敗した原因が分かりやすい。
この記事ではpytestの使い方に関して、公式のドキュメントを参考にメモする。
インストール
pipなどを使用してインストールする。
pip install pytest
基本的な使い方
基本的にはassert
で望む結果を書く。
ここではtest_assert1.py
というテスト用のファイルを作成する。test_
で始まる関数がテスト対象となる。ディスカバリーのルールに関してはこの記事の最後に明記する。
# content of test_assert1.py
def f():
return 3
def test_function():
assert f() == 4
テストの実行はpytest
コマンドを利用する。
上記は、f()
の結果が4
でないため、Failureとなる。
$ pytest test_assert1.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile:
collected 1 item
test_assert1.py F [100%]
================================= FAILURES =================================
______________________________ test_function _______________________________
def test_function():
> assert f() == 4
E assert 3 == 4
E + where 3 = f()
test_assert1.py:5: AssertionError
========================= 1 failed in 0.12 seconds =========================
期待される例外の処理
例外が発生することが期待される場合にはpytest.raises
を利用する。
以下の通りすると、ZeroDivisionError
が発生すればテスト成功、発生しなければテスト失敗となる。
import pytest
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
実際の例外にアクセスしたい場合は以下の通りに行う。
import pytest
def test_recursion_depth():
with pytest.raises(RuntimeError) as excinfo:
def f():
f()
f()
assert 'maximum recursion' in str(excinfo.value)
fixtures
fixturesはunittestのsetup/teardownのような関数を劇的に改善する関数である。
関数の引数としてのFixtures
テスト関数は引数として名前を指定することでfixtureオブジェクトを受け取ることができる。それぞれの引数名に対して、その名前を持つfixture関数がfixtureオブジェクトを提供する。fixture関数は@pytest.fixture
とともに作成することで登録される。
以下がシンプルな例である。
# content of ./test_smtpsimple.py
import pytest
@pytest.fixture
def smtp_connection():
import smtplib
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert 0 # for demo purposes
conftest.pyを利用したfixture関数の共有
fixture関数を複数のテストファイルから使用したいならば、それをconftest.py
ファイルに移動するとよい。テストファイルの中で明示的にconftest.py
をimportする必要はなく、自動的にpytestによって発見される。
fixture関数のディスカバリーはテスト関数から始まり、テストモジュール、conftest.py
、最後に組み込み、サードパーティープラグインの順に行われる。
テストデータの共有
もしテストデータをファイルから利用したい場合は、fixtureの中でそれらのデータを読み込むのは良い方法である。これはpytestの自動キャッシュ機構を利用することができる。
他の良いアプローチはtests
フォルダーの中にデータファイルを追加することである。pytest-datadirやpytest-datafilesのようなプラグインを利用すれば、このようなテストを管理することを助ける。
テスト間・クラス間・セッション間のfixtureインスタンスの共有
fixtureのscope引数にmoduleを指定すると、テストモジュールごとに1度fixturesが呼び出される。(デフォルトではテスト関数ごとに1度)
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
もし、全てのテストで共有したい場合はsessionを指定する。
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests needing it
...
Fixtureの終了処理
returnの代わりにyieldを使用すると、yield文以降の全てのコードは終了処理コードとして扱われる。
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
yield smtp_connection # provide the fixture value
print("teardown smtp")
smtp_connection.close()
with文を使用してシームレスにyieldを使用することも可能である。
# content of test_yield2.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
yield smtp_connection # provide the fixture value
ただし、もしyieldの前に例外が発生した場合は、yield以降のコードが実行されない。
終了処理コードを実行するためのもうひとつのオプションは、finalization関数を登録するために、リクエストコンテキストのaddfinalizer
メソッドを使用することである。
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection(request):
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def fin():
print("teardown smtp_connection")
smtp_connection.close()
request.addfinalizer(fin)
return smtp_connection # provide the fixture value
yieldとaddfinalizerメソッドは似ているが、2つの点でaddfinalizerはyieldとは異なる。
- 複数のfinalizer関数を登録することが出来る。
- finalizersはsetupコードが例外を挙げたかどうかによらず、いつも呼ばれる。これはfixtureによって生成された全てのリソースを適切にクローズするのに便利である。
@pytest.fixture
def equipments(request):
r = []
for port in ('C1', 'C3', 'C28'):
equip = connect(port)
request.addfinalizer(equip.disconnect)
r.append(equip)
return r
上記の例で、もしC28が例外を伴って失敗したとしても、C1とC3は適切にクローズされる。もちろん、もしfinalize関数が登録される前に例外が発生した場合には、それが実行されることはない。
fixture関数をfixture関数から利用する
fixtureはfixtureからも利用できる。これはfixtureをモジュラーデザインにすることに貢献し、多くのプロジェクトに渡ってフレームワーク固有の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
上記の例では、app fixtureを定義し、それは前もって定義されたsmtp_connection
fixtureを受け取り、App
オブジェクトとともにインスタンス化される。
パラメータ化したfixtures
@pytest.mark.parametrizeを利用すれば、テスト関数のための引数をパラメータ化することができる。
# content of test_expectation.py
import pytest
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
("6*9", 42),
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
上記の例では3つの(test_input, expected)
というタプルを定義しており、test_eval
関数はそれらを使用して3回実行される。
複数のパラメータの全ての組み合わせを試験したい場合は、parametrize
をスタックできる。
import pytest
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
pass
上記の例では、x=0/y=2, x=1/y=2, x=0/y=3, and x=1/y=3の組み合わせでテストされる。
一時的なファイルの利用
tmpdir
fixtureを利用すれば、一時的なディレクトリを利用することが出来る。
tmpdir
はpy.path.localオブジェクトであり、それはos.path
などのメソッドを提供する。以下は使用例である。
# content of test_tmpdir.py
import os
def test_create_file(tmpdir):
p = tmpdir.mkdir("sub").join("hello.txt")
p.write("content")
assert p.read() == "content"
assert len(tmpdir.listdir()) == 1
assert 0
tmpdir_factory
tmpdir_factory
はsessionのスコープで動作するfixtureであり、どんなfixtureやテストからも利用できる任意の一時的なディレクトリを作るのに使用できる。
例えば、以下の例では、大きなサイズのイメージをテストごとに作成する代わりに、セッションごとに1度生成することで時間を抑えることが出来る。
# contents of conftest.py
import pytest
@pytest.fixture(scope="session")
def image_file(tmpdir_factory):
img = compute_expensive_image()
fn = tmpdir_factory.mktemp("data").join("img.png")
img.save(str(fn))
return fn
# contents of test_image.py
def test_histogram(image_file):
img = load_image(image_file)
# compute and test histogram
monkey patching / mock
テスト対象となるコードの一部を差し替えるために、monkey patchingが使用できる。
monkey patchingを利用すれば、ソースコード内で呼び出される関数やアイテム、環境変数などを設定、削除することができる。
以下はシンプルな例である。以下ではos.path.expanduser
の関数がtest_mytest
内で定義されるmockreturn
に置き換えられる。
# content of test_module.py
import os.path
def getssh(): # pseudo application code
return os.path.join(os.path.expanduser("~admin"), '.ssh')
def test_mytest(monkeypatch):
def mockreturn(path):
return '/abc'
monkeypatch.setattr(os.path, 'expanduser', mockreturn)
x = getssh()
assert x == '/abc/.ssh'
次に以下の例では、request.session.Session.request
メソッドが削除され、http requestが作成されるとテストに失敗するようになることが予想される。
# content of conftest.py
import pytest
@pytest.fixture(autouse=True)
def no_requests(monkeypatch):
monkeypatch.delattr("requests.sessions.Session.request")
よいアプリケーション構成
標準的なdiscovery
pytestは以下のようなディスカバリーを実装している。
- 引数が何も指定されていない場合は、testpaths(もし設定されていれば)かカレントディレクトリからテストファイルを捜索する
- かわりにディレクトリ、ファイル名の組み合わせを引数に利用することも出来る
- norecursedirsにマッチしない限り、ディレクトリは再帰的に捜索される
- それぞれのディレクトリでは
test_*.py
もしくは*_test.py
に一致するファイルが捜索され、それらのテストパッケージがインポートされる - それぞれのファイルからテストアイテムが集められる
-
test_
というプリフィックスが付けられたクラス外の関数、もしくはメソッド -
Test
というプリフィックスが付いたクラス内の中で、test_
というプリフィックスが付いた関数やメソッド(__init__
メソッドは除く)
-
テストレイアウト/インポートルールの選択
pytestは以下の2つのレイアウトをサポートする。
- アプリケーションコード外のテスト
- アプリケーションコードの一部としてのテスト
ここでは「アプリケーションコード外のテスト」のみを扱う。
以下のような構成にすると、インストールされたバージョンのmypkgに対して簡単にテストを実行することが可能である。
setup.py
mypkg/
__init__.py
app.py
view.py
tests/
test_app.py
test_view.py
...
このようなスキームを使用する場合、テストファイルは必ずユニークな名前にしなければならない。なぜならば、完全なパッケージ名を取得するためのパッケージがないため、pytest
はそれらをトップレベルのモジュールとしてインポートするからである。言い換えるならば、sys.path
にtests/
を追加することにより、上記の例のテストファイルは、トップレベルモジュールのtest_app
とtest_view
としてインポートされる。
もし、同じ名前のテストモジュールが必要であるならば、それらをパッケージにするために、__init__.py
をtests
フォルダーとサブフォルダーに追加するであろう。
setup.py
mypkg/
...
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
この場合、pytestがtests.foo.test_view
とtests.bar.test_view
としてモジュールをロードして、同じ名前のモジュールを持つことが可能となる。しかし、これだとちょっとした問題を引き起こす。それは、tests
ディレクトリからモジュールをロードするために、pytestがsys.path
の先頭にレポジトリのルートを追加するが、これがmypkg
もインポート可能となるということを引き起こすからである。
もし仮想環境においてパッケージをテストするためにtoxのようなツールを利用しているならば、これは問題となる。なぜなら、テストしたいのはインストールされたバージョンのパッケージであり、レポジトリのローカルコードではないからだ。
このようなシチュエーションではsrc
レイアウトを使用することが強く推奨される。その場合、アプリケーションのルートパッケージは、レポジトリのルートのサブディレクトリの中に居座り続ける。
setup.py
src/
mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
このレイアウトの有効性に関しては以下のブログに記載されている。