Pytestを実装する
Python のテストとして馴染み深いライブラリで「Pytest」があります。
Pytest は、ユニットテストを支援するためのモジュールとして準備されています。
また、一般的にテストは「ユニットテスト」と「結合テスト」というものが存在します。
例えば、DB の Model に書かれているメソッドやデータ登録が想定した結果になることを確認するのがユニットテストとして扱われています。
あくまでシステム全体としてというよりは、機能として問題ないかをチェックするテストです。
一方、結合テストはユニットテストを経た構成要素を結合し、サブシステムとしてまとまった単位で動作のチェックを行うことを指します。
システム内の機能が連携できているか検証できます。
ユニットテストのメリットは、問題点を特定し易くシステムとして独立してテスト可能であるため、テストを書くのに工数がかかりません。
一方、システム全体の動作を保証するものではないため、その保証ができるのは結合テストのメリットです。
その分結合テストは、デメリットとして時間がかかってしまいます。
Pytestの導入
それでは、実際に pytest を導入してみましょう。
Python の環境はできている想定で進めていきます。
もしできていない人は、venv
などを利用しながら開発仮想環境を構築してください。
$ pip install pytest
$ pytest --version
pytest 7.4.0
version 7.4 が入りました。
pytest では、ファイル名に test_*.py や *_test.py とすることで自動的にテストファイルと認識してくれます。
どこのディレクトリにおいても上記ファイルを認識しますが、できればテストファイルを管理したいので、tests/を作ると良いです。
test_basic.py として下記を作ります。
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
下記を実行し、失敗することを確認しましょう。
$ pytest
==================================== test session starts ====================================
platform darwin -- Python 3.9.4, pytest-7.4.0, pluggy-1.2.0
rootdir: /streamlit_pytest
collected 1 item
tests/test_basic.py F [100%]
==================================== FAILURES ====================================
____________________________________________________________________________________ test_answer _____________________________________________________________________________________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
tests/test_basic.py:6: AssertionError
==================================== short test summary info ====================================
FAILED tests/test_basic.py::test_answer - assert 4 == 5
==================================== 1 failed in 0.04s ====================================
例外処理が起きることもチェック可能です。
import pytest
def f():
raise SystemExit(1)
def test_mytest():
with pytest.raises(SystemExit):
f()
テストのグループ化
pytest では、複数のテストを含むクラスを簡単に作成できます。
class TestClass:
def test_one(self):
x = "this"
assert "h" in x
def test_two(self):
x = "hello"
assert hasattr(x, "check")
デバッグ
テストファイル内で print
を書いても出力されません。
下記コマンドで出力できるようになります。
$ pytest -s
オプションはたくさんありますが、下記でオプション一覧を確認可能です。
$ pytest --help
プラグイン
pytest に導入すると便利なプラグインを紹介します。
pytest-pycodestyle
--pep8
PEP8 準拠チェックを有効にするプラグインとして、pytest-pycodestyle
があります。
元々は、pytest-pep8
というプラグインでした。
$ pip install pytest-pycodestyle
$ pytest --pycodestyle ...
設定をこちらで変更可能です。
[pycodestyle]
max-line-length = 127
[tool:pytest]
# これでpytestだけでデフォルトコマンドを指定できるようです。
addopts = --pycodestyle
pyflakes
pyflakes でソースコードをチェックします。
$ pip install pytest-flakes
$ pytest --flakes
==================================== test session starts ====================================
platform darwin -- Python 3.9.4, pytest-7.4.0, pluggy-1.2.0
rootdir: /Users/naoki/Documents/matsuzaki-app/streamlit_pytest
plugins: flakes-4.0.5
collected 5 items
main.py s [ 20%]
pages/page1.py s [ 40%]
pages/page2.py s [ 60%]
tests/test_basic.py .. [100%]
==================================== 2 passed, 3 skipped in 0.01s ====================================
Pytestの細かな機能
ここからは一歩進んだ Pytest の機能を紹介します。
Pytestの fixturesについて
@pytest.fixture
デコレータを利用することで、それぞれのテストで前処理と後処理が利用できるようになります。
import pytest
class TestFixtures:
@pytest.fixture()
def sample(self):
print("前処理")
yield
print("後処理")
def test_hoge(self, sample):
print("本処理")
assert 1 == 1
こうすることで、test_hoge
関数が呼ばれた際、引数にsample
が渡されているので、sample
関数が呼ばれます。
sample
関数には、yield
が書かれており、ここでtest_hoge
の処理が実行されるようになります。
いわゆる、fixtures によって前処理と後処理を実行できるようになります。
fixtures には、scope が定義できます。
scope指定 | 詳細 |
---|---|
scope=’function’ | テスト関数ごとに、一回実行(デフォルト) |
scope=’class’ | テストクラスごとに、一回実行 |
scope=’module’ | モジュール(ファイル)ごとに、一回実行 |
scope=’package’ | パッケージごとに、一回実行 |
scope=’セッション’ | セッション(pytestコマンドでテストを実施)ごとに、一回実行。 |
import pytest
@pytest.fixture(scope="function")
def fixture_function():
"""
テスト関数ごとに、一回実行
"""
print("前処理: function")
yield
print("後処理: function")
@pytest.fixture(scope="class")
def fixture_class():
"""
テストクラスごとに、一回実行
"""
print("前処理: class")
yield
print("後処理: class")
@pytest.fixture(scope="module")
def fixture_module():
"""
モジュール(ファイル)ごとに、一回実行
"""
print("前処理: module")
yield
print("後処理: module")
@pytest.fixture(scope="session")
def fixture_session():
"""
セッション(pytestコマンドでテストを実施)ごとに、一回実行
"""
print("前処理: session")
yield
print("後処理: session")
class TestFixtureScope_2:
def test_1(self, fixture_function, fixture_class, fixture_module, fixture_session):
pass
def test_2(self, fixture_function, fixture_class, fixture_module, fixture_session):
pass
class TestFixtureScope_1:
def test_1(self, fixture_function, fixture_class, fixture_module, fixture_session):
pass
def test_2(self, fixture_function, fixture_class, fixture_module, fixture_session):
pass
$ pytest
tests/unit/test_scope.py ..前処理: session
前処理: module
前処理: class
前処理: function
.後処理: function
前処理: function
.後処理: function
後処理: class
前処理: class
前処理: function
.後処理: function
前処理: function
.後処理: function
後処理: class
後処理: module
後処理: session
このように、scope によって実行単位を変えることが可能です。
fixturesを共有
複数のテストファイルで fixture を共有したい時は、conftest.py
に定義すれば、簡単に共有ができます。
conftest.py
のサンプルは以下の通りです。
import pytest
@pytest.fixture(scope="session")
def conf():
print("conftest:前処理")
yield
print("conftest:後処理")
あとは利用側で conf
を引数に渡してあげるだけです。
Pytestの Autouse fixturesについて
すべてのテストで依存することがわかっているフィクスチャ (あるいは複数) を用意したいことがあります。
autouse
fixtures は、すべてのテストが自動的にそれらをリクエストする便利な方法です。
これにより、冗長なリクエストの多くをカットできます。
fixtures のデコレータに autouse=True を渡すことで、フィクスチャを自動生成フィクスチャにできます。
import pytest
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return []
@pytest.fixture(autouse=True)
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(order, first_entry):
assert order == [first_entry]
def test_string_and_int(order, first_entry):
order.append(2)
assert order == [first_entry, 2]
上記のように autouse=True
を書くことで、引数にその関数を渡さなくても、テストで必ず実行されます。
例えば、test_string_only
関数で、まず初めに append_first
関数が呼ばれます。
そのappend_first
では、order に “a”を入れる処理がされています。
そのため、 order == [first_entry]
が true となるのです。
テスト関数のパラメータ化
パラメータ化という概念で、@pytest.mark.parametrize
がありますが、ここでは説明を省きます。
E2Eテストとは
End to End:エンドツーエンドの略です。
あらゆる構成要素を組み込んだ状態で、テスト対象システム全体の品質を保証する手法です。
UI 操作をともなうことから「UI テスト」と呼ばれる場合もありますが、バックエンドとの通信などもテスト観点に含まれるため、より上位の概念といえます。
テストの中でも実際のユーザーの動きに近いテストが可能となるため、品質を担保できるテストとも言えます。
課題としては、やはり導入コストがかかることです。
しかし、この playwright は Pytest で実施できるためのプラグインを用意してくれているため、それを使えば簡単に導入できそうです。
Pytestにブラウザテストを導入
ブラウザテストで、「playwright」というものがあります。
これを利用することで、ブラウザ遷移等の E2E テストが可能となります。
Playwright は、Chromium、WebKit、Firefox などの最新のレンダリング エンジンをすべてサポートしています。
Windows、Linux、macOS 上で、ローカルまたは CI 上で、ヘッドレスまたはネイティブ モバイル エミュレーションを使用したヘッドでテストします。
PytestとPlaywrightの利用
Playwright では、公式のPlaywright Pytest プラグインを使用してエンドツーエンドのテストを作成することをお勧めします。
コンテキストの分離を提供し、すぐに複数のブラウザー構成で実行できます。
Pytest プラグインは Playwright の同期バージョンを利用します。
インストールと準備
$ pip install pytest-playwright
必要なブラウザをインストールします。
$ playwright install
あとは、pytest のようにテストコードを書いていけば OK です。
今回は tests/browser/test_basic.py と設定します。
サンプルを掲載します。
import re
from playwright.sync_api import Page, expect
def test_homepage_has_Playwright_in_title_and_get_started_link_linking_to_the_intro_page(page: Page):
page.goto("https://playwright.dev/")
# Expect a title "to contain" a substring.
expect(page).to_have_title(re.compile("Playwright"))
# create a locator
get_started = page.get_by_role("link", name="Get started")
# Expect an attribute "to be strictly equal" to the value.
expect(get_started).to_have_attribute("href", "/docs/intro")
# Click the get started link.
get_started.click()
# Expects the URL to contain intro.
expect(page).to_have_url(re.compile(".*intro"))
to_have_title
や to_have_url
はページに対するアサーションとなっています。
- 指定したタイトルを持っているか
- 指定した URL になっているか
をチェックします。
一方、to_have_attribute
や to_be_checked
は、ロケーターに対するアサーションとなっています。
- 要素に指定した属性を持ち合わせているか
- 要素(ここではチェックボックス)はチェックされた状態であるか
をチェックします。
この辺りの他のアサーションに関しては、下記ページを参考ください。
ロケーターの指定
また、ロケーターをどうやって検索すれば良いかについては、下記が参考になります。
get_by_role
で取得するための role の指定は様々あります。
ロール ロケーターには、ボタン、チェックボックス、見出し、リンク、リスト、テーブルなどが含まれており、 ARIA ロール、ARIA 属性、およびアクセス可能な名前の W3C 仕様に従っています。
css
でも指定できます。
<button style='display: none'>Invisible</button>
<button>Visible</button>
page.locator("button").locator("visible=true").click()
expect では、第二引数にエラー時のメッセージを表示させることが可能です。
expect(page.get_by_text("Name"), "should be logged in").to_be_visible()
def test_foobar(page: Page) -> None:
> expect(page.get_by_text("Name"), "should be logged in").to_be_visible()
E AssertionError: should be logged in
E Actual value: None
E Call log:
E LocatorAssertions.to_be_visible with timeout 5000ms
E waiting for get_by_text("Name")
E waiting for get_by_text("Name")
tests/test_foobar.py:22: AssertionError
ロケーターのフィルタリング
<ul>
<li>
<h3>Product 1</h3>
<button>Add to cart</button>
</li>
<li>
<h3>Product 2</h3>
<button>Add to cart</button>
</li>
</ul>
例えば上記のように button が 2 つあると、get_by_role(”button”)で探すと 2 つヒットします。
その場合、下記のようにfilter
メソッドを使うことで 1 つに絞ることができます。
page.get_by_role("listitem").filter(has_text="Product 2").get_by_role(
"button", name="Add to cart"
).click()
これは、まず listitem(<li>
タグ)を filter で “Product 2”を持っている li タグに絞り、その中で、button を探しています。
スコープを狭めることもできるということですね。
filter に4方法使えます。
- has_text
- has_not_text
- has
- has_not
has や has_not は、中の要素で検索するときにつかいます。
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
).get_by_role("button", name="Add to cart").click()
こうすることで、<li>
タグの中で、Product 2 の heading の role を持つタグに絞り込めます。
ロケーターを利用したイベント
テスト実行
pytest 同様に実行できます。
(-s をつけてデバッグできるようにします)
$ pytest -s
下記のように実行することで、Playwright Inspector が起動し、ステップ毎に実行できます。
$ PWDEBUG=1 pytest -s
また、pytest はデフォルトがヘッドレスモードなので、下記でブラウザ起動ができます。
$ pytest --headed
特定のブラウザで実行したい場合は、 ブラウザを指定できます。
$ pytest --browser webkit --browser firefox
失敗時にスクリーンショットを撮り、そのファイルを置く場所を指定も可能です。
$ pytest --screenshot only-on-failure --output ./test-results/
テストを並行して実行する場合はこちらです、ただこれは、pytest-xdist
がインストールされている必要があります。
$ pytest --numprocesses auto
並行処理については、詳しくは説明しません。
下記を参照ください。
- https://playwright.dev/python/docs/test-runners#parallelism-running-multiple-tests-at-once
- https://qiita.com/yaboxi_/items/0cdc2818bf8acf6f00de
- https://rcmdnk.com/blog/2023/03/13/computer-python/
テストジェネレーターの利用
playwright では、実際の操作を記録し、それをテストコードとして利用できる codegen という機能があります。
その後にテストを生成する Web サイトの URL を指定します。
URL はオプションであり、いつでも URL を指定せずにコマンドを実行し、代わりにブラウザ ウィンドウで URL を直接追加できます。
$ playwright codegen demo.playwright.dev/todomvc
ベースURLの設定
URL を設定することで下記のように可能です。
pytest --base-url http://localhost:8080
def test_visit_example(page):
page.goto("/admin")
# -> Will result in http://localhost:8080/admin
まとめ
pytest やブラウザテストツールの playwright をうまく使いこなすようになるためには時間がかかりそうですが、ブラウザテストも pytest でできるのはとても便利だと感じます。
もう少し知見を増やし、テストコードを書くことによる品質担保に努力したいです。
Discussion