『ハイパーモダンPython』で紹介されていたpytestのプラグインを試してみる
2024年9月に出版された技術書『ハイパーモダンPython―信頼性の高いワークフローを構築するモダンテクニック』では、Pythonのテストツールであるpytestの利用方法に加えて、pytestのプラグインも多数紹介されています。
この記事では、これらのプラグインの機能概要と実際に導入してみた例を紹介します。
なおpytestとプラグインに関する基本的な説明は割愛します。これらの詳細は書籍内の「6章:pytestによるテスト」内で紹介されていますので、そちらも合わせてご参照ください。
紹介するプラグインの一覧
以下に、今回紹介するプラグインの名称とその概要をまとめます。PyPIへのリンクも掲載しますので、より正確な情報は各プラグインのドキュメントを参照してください。
プラグイン名 | 概要 |
---|---|
pytest-xdist | テストの並列実行をサポートし、テストの速度向上を図るためのプラグイン。 |
pytest-cov | カバレッジ測定を行い、テストがどれだけのコードをカバーしているかを確認できるプラグイン。 |
xdoctest | docstringのテストを行うプラグイン。 |
pytest-sugar | テスト実行のプログレスバーを表示するためのプラグイン。 |
pytest-icdiff | テスト失敗時に辞書やリストなどの差分を表示するプラグイン。 |
pytest-httpserver | HTTPリクエストをモックするためのプラグイン。外部APIとの連携テストを効率的に行える。 |
pytest-factoryboy | FakerやFactoryBoyを使ってテスト用のランダムなダミーデータを簡単に生成できるプラグイン。 |
pytest-datadir | テスト用の外部データファイル(txtやcsvなど)を読み込み、データの取得やファイル更新を実行できるプラグイン。 |
ソースコード
今回のサンプルコードはGitHubに掲載しています。
プロジェクト管理にはuvを活用して、プラグインの導入などを行いました。pyproject.toml
ファイルは以下のようになっています。
[project]
name = "todo_app"
version = "0.1.0"
dependencies = [
"requests",
]
[tool.uv]
dev-dependencies = [
"pytest",
"pytest-xdist",
"pytest-sugar",
"pytest-icdiff",
"pytest-httpserver",
"pytest-factoryboy",
"pytest-datadir",
"pytest-cov",
"xdoctest",
"pygments",
]
[tool.coverage.run]
source = ["pytest_plugin_demo", "tests"]
omit = ["*/__init__.py"]
[tool.coverage.report]
show_missing = true
ソースツリーは以下のような構成にしています。todo_service.py
がテスト対象となるソースコードです。
.
├── README.md
├── pyproject.toml
├── src
│ ├── pytest_plugin_demo
│ │ ├── __init__.py
│ │ └── todo_service.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── data
│ │ └── test_tasks.csv
│ └── test_todo_service.py
└── uv.lock
プロジェクトの直下で以下のようにコマンドを実行することで、pytestを実行できます。(一部のプラグインを実行する場合は、後述するオプションも付与します)
$ uv sync
$ uv run pytest
サンプルプログラム
次に、pytestのプラグインを使ったサンプルプログラムを示します。TODOリスト管理アプリを題材に、プラグインを組み合わせたテストを実行します。
テスト対象のモジュール
TODOリストのタスクを表すTask
クラスと、CSVファイルからタスクを読み込むload_tasks_from_csv()
関数を実装し、これらをテストします。xdoctest
を活用するためにdocstringも記載しています。
import csv
from typing import List
class Task:
"""タスクを表すクラス。
Attributes:
task (str): タスクの名前
completed (bool): タスクの完了状態
Example:
>>> task = Task("Example Task")
>>> task.completed
False
>>> task.complete()
>>> task.completed
True
"""
def __init__(self, task: str, completed: bool = False):
self.task = task
self.completed = completed
def complete(self):
self.completed = True
def load_tasks_from_csv(filepath: str) -> List[Task]:
"""CSVファイルからタスクを読み込む。
Example:
>>> tasks = load_tasks_from_csv("tests/data/test_tasks.csv")
>>> len(tasks)
2
"""
with open(filepath, newline="") as csvfile:
reader = csv.DictReader(csvfile)
return [Task(row["task"], row["completed"] == "True") for row in reader]
テストコード
次にテストコードを実装します。pytest-icdiff
はテストの失敗時に効果を発揮するテストのため、わざと失敗させるようにします。
それぞれのテストの詳細は後述します。
from src.pytest_plugin_demo.todo_service import Task, load_tasks_from_csv
from tests.conftest import TaskFactory
# pytest-httpserver 用のテスト
def test_httpserver(httpserver):
test_response = [{"task": "API Task", "completed": False}]
httpserver.expect_request("/tasks").respond_with_json(test_response)
import requests
response = requests.get(httpserver.url_for("/tasks")).json()
assert response[0]["task"] == "API Task"
# pytest-factoryboy 用のテスト
def test_task_factory():
for _ in range(100000): # 時間のかかるテスト
task = TaskFactory()
assert isinstance(task, Task)
assert isinstance(task.completed, bool)
def test_task_factory_2():
for _ in range(100000): # 時間のかかるテスト(並列実行用)
task = TaskFactory()
assert isinstance(task, Task)
assert isinstance(task.completed, bool)
# pytest-datadir データファイルの使用
def test_load_tasks_from_csv(shared_datadir):
csv_file = shared_datadir / "test_tasks.csv"
tasks = load_tasks_from_csv(str(csv_file))
assert len(tasks) == 2
# pytest-icdiff 差分比較
def test_task_comparison():
task1 = Task("Task A")
task2 = Task("Task B")
assert task1.__dict__ == task2.__dict__ # 故意に失敗させる
ここからは、pytestのプラグインを3つの種類に分けて紹介します。
テスト実行方法に関連するプラグイン
pytest-xdist
pytest-xdistは、テストを並列実行するためのプラグインです。これを活用すると、テストの実行速度を向上できる場合があります。以下のように、-n auto オプションでテストを並列実行します。
pytest -n auto
今回は、一部のテストの実行時間が長くなるように調整しています。この状態でのテスト実行時間を比較してみます。
# pytest-factoryboy 用のテスト
def test_task_factory():
for _ in range(100000): # 時間のかかるテスト
task = TaskFactory()
assert isinstance(task, Task)
assert isinstance(task.completed, bool)
def test_task_factory_2():
for _ in range(100000): # 時間のかかるテスト(並列実行用)
task = TaskFactory()
assert isinstance(task, Task)
assert isinstance(task.completed, bool)
以下のように、半減とまではいきませんが、テストの実行時間を削減できました(4.68sec→3.22sec)。もっとテストケースが多い場合はさらに効果が見込めると思います。
$ uv run pytest # 通常実行
(略)
Results (4.68s):
4 passed
1 failed
- tests/test_todo_service.py:39 test_task_comparison
# uv run pytest -n auto # 並列実行
(略)
Results (3.22s):
4 passed
1 failed
- tests/test_todo_service.py:39 test_task_comparison
テストの実行時間が長くなってきた時には導入を検討したいプラグインです。
pytest-cov
pytest-cov は、コードのカバレッジを測定するためのプラグインです。テストがカバーしているコードの割合をレポートとして表示してくれます。
以下のオプションを付与してテストを実行します。
pytest --cov
今回はカバレッジが100%になりました。(pyproject.tomlの設定で、__init__.py
を計測対象外にしています)
---------- coverage: platform darwin, python 3.12.2-final-0 ----------
Name Stmts Miss Cover Missing
----------------------------------------------------------
tests/conftest.py 7 0 100%
tests/test_todo_service.py 26 0 100%
----------------------------------------------------------
TOTAL 33 0 100%
このプラグインは、内部的にはCoverage.py
というツールを実行しています。詳細な設定方法などはそちらのドキュメントを参照するとよさそうです。
xdoctest
xdoctest は、docstringに記載したExampleの内容をテストします。単体でも利用できますが、--xdoctest
オプションをつけて実行することで、pytestのテストと同時に実行できます。
$ uv run pytest --xdoctest
実行結果を見ると、テストケースの中にdocstringのテストが含まれていることがわかります。(パスしたテストケース数が4から6に増加)
src/pytest_plugin_demo/todo_service.py ✓✓
(略)
Results (4.68s):
6 passed
1 failed
- tests/test_todo_service.py:39 test_task_comparison
ここまでのプラグインをすべて同時に利用する場合、pytestのオプションは以下のようになります。
$ uv run pytest --xdoctest --cov -n auto
テストの実行結果に関連するプラグイン
続いては、テストの実行結果を見やすくするプラグインです。どんな場合でも汎用的に活用できます。
pytest-sugar
pytest-sugar はテストの進行状況を視覚的に表示し、進行状況バーや成功・失敗したテストを色分けして表示します。これにより、テストの結果が直感的にわかりやすくなります。プラグインを導入するだけで自動的に適用されます。
右上の「100%」の箇所がプログレスバー。失敗したテストは赤で表示される
テストの実行に時間がかかるプロジェクトの場合は導入しておくと良いかもしれません。
pytest-icdiff
pytest-icdiff を使うと、辞書やリストなどの比較時に、差分を見やすく表示できます。これもプラグインを導入するだけで自動的に適用されます。
差分が出ている箇所だけが強調して表示されるようになりました。項目数の多い辞書を扱う場合はとくに役立ちそうです。
テストコードの実装に関連するプラグイン
最後に、テストコードの実装を容易にするプラグインです。テストする内容に応じて導入します。
pytest-httpserver
pytest-httpserver は、HTTPリクエストをモックするためのプラグインです。外部APIのレスポンスをテストする際、事前に設定した内容を返すモックサーバーを構築できます。
以下の例のように、外部APIのURLを指定する箇所を、httpserver
で置き換えることができます。
def test_httpserver(httpserver):
test_response = [{"task": "API Task", "completed": False}]
httpserver.expect_request("/tasks").respond_with_json(test_response)
import requests
response = requests.get(httpserver.url_for("/tasks")).json()
assert response[0]["task"] == "API Task"
外部APIと連携するプロジェクトを作成する際には積極的に活用したいプラグインです。
pytest-factoryboy
pytest-factoryboy は、ランダムなテスト用データを生成するためのプラグインです。
まず、以下のようにTaskFactory
クラスを定義します。今回は、タスクのタイトルとしてランダムな3つの単語が設定されるようなFactoryを定義しています。
import factory
from src.pytest_plugin_demo.todo_service import Task
class TaskFactory(factory.Factory):
class Meta:
model = Task
task = factory.Faker("sentence", nb_words=3)
completed = factory.Faker("boolean")
テストコード内では先ほど定義したTaskFactory
クラスを呼び出すことで、ランダムな値が設定されたTask
クラスを生成できます。そのクラスを利用してテストを実行します。
def test_task_factory():
task = TaskFactory()
assert isinstance(task, Task)
assert isinstance(task.completed, bool)
試しにtask
の中身を出力してみたところ、以下のようにランダムな値が設定されていました。
task=Hot economic. comp=True
task=Allow marriage. comp=False
task=Over eight. comp=True
task=Sing than light. comp=False
task=Baby then. comp=False
task=Response news. comp=False
task=Wife score. comp=False
pytest-datadir
pytest-datadir は、テストで使う外部ファイルを簡単に管理するためのプラグインです。
以下の例のように、datadir
を指定することで、data/
ディレクトリ内のデータにアクセスできます。
def test_load_tasks_from_csv(datadir):
csv_file = datadir / "test_tasks.csv"
tasks = load_tasks_from_csv(str(csv_file))
assert len(tasks) == 2
このプラグインのポイントは、テストの実行時にファイルを一時フォルダにコピーして利用する点です。そのため、ファイルの内容を変更するようなテストにおいて効果を発揮します。
まとめ
この記事では、pytestの便利なプラグインを、実用例を交えて紹介しました。それぞれのプラグインをうまく活用することで、テストコードの記載を容易にし、テストの実行も効率化できます。
pytestには豊富なプラグインがあることは知っていましたが、実際にどのようなものがあるか、体型的には把握できていませんでした。今回の検証を通じて、それぞれのプラグインのメリットを実感できました。
この中でも、テスト結果の差分を見やすくするpytest-icdiffは、とりあえず導入してみたいと思いました。その他のプラグインは必要に応じて導入を検討したいです。
参考文献
- 『Hypermodern Python Tooling』Claudio Jolowicz、O'Reilly、Copyright 2024 Claudio Jolowicz、978-1-098-13958-2、邦題『ハイパーモダンPython』オライリー・ジャパン、ISBN978-4-8144-0092-8
Discussion