概要
pytest で unit / integration / e2e テストを運用するときに、ディレクトリで実行対象を分けるべきか、pytest のマーカーで制御するべきかを整理します。
今回は、以下のような前提のプロジェクトを想定します。
-
tests/unit,tests/integration,tests/e2eでディレクトリを分けている -
pyproject.tomlでintegration/e2eマーカーを登録している - CI では
uv run pytest tests/unit -vのようにディレクトリ単位で実行している
結論としては、CI の主制御はディレクトリで行い、マーカーは補助的に使うのが扱いやすいです。
対象読者
- pytest のマーカー運用を整理したい方
-
unit/integration/e2eテストを分けて運用したい方 - CI でどのようにテストを実行対象ごとに分離するか悩んでいる方
前提条件
今回の前提となる pytest 設定は以下です。
[tool.pytest.ini_options]
# markerの誤字を検知するため、未登録markerはエラーにする
addopts = "--strict-markers"
markers = [
"integration: Databricks Connect経由でServerless computeに接続する統合テスト",
"e2e: Databricks Asset Bundles経由でジョブ/パイプラインを実行するE2Eテスト",
]
また、テストディレクトリは以下のように分かれている想定です。
tests/
├─ unit/
├─ integration/
└─ e2e/
結論
今回のように tests/unit, tests/integration, tests/e2e で物理的に分かれている構成 なら、方針は次のようにするのが妥当です。
-
CI の実行対象制御はディレクトリで行う
uv run pytest tests/unit -vuv run pytest tests/integration -vuv run pytest tests/e2e -v
-
integration/e2eマーカーは残す- テストの意味をコード上で明示できる
-
--strict-markersにより誤字や誤分類を検知できる - 将来的に
-mで柔軟に絞り込める
-
unit テストは現時点ではマーカーなしでもよい
-
tests/unitという配置自体が unit テストであることを表せるため
-
つまり、実行制御はディレクトリ、意味付けと保険としてマーカーという役割分担にすると整理しやすいです。
背景・課題
pytest では、テストを絞り込む方法として大きく次の 2 つがあります。
-
ディレクトリで分ける
pytest tests/unit
-
マーカーで分ける
pytest -m integration
一見するとどちらでも実現できますが、両者は役割が少し異なります。
特に次のような状態だと迷いやすくなります。
- すでに
tests/unit,tests/integration,tests/e2eに分けている - さらに
@pytest.mark.integrationや@pytest.mark.e2eも使っている - CI ではどちらを基準に実行するべきか決めたい
解決方法
方法概要
おすすめの役割分担は次のとおりです。
| 目的 | 使うもの |
|---|---|
| CI で unit / integration / e2e を分けて実行する | ディレクトリ |
| テストの意味をコード上で明示する | マーカー |
| typo や未登録マーカーを検知する | --strict-markers |
| 将来的に細かく絞り込む | pytest -m ... |
なぜディレクトリ主導がよいのか
1. CI の意図が分かりやすい
たとえば CI で unit テストを実行するなら、次のコマンドはかなり直感的です。
uv run pytest tests/unit -v
一方で、マーカーだけで unit テストを表現しようとすると、たとえば次のようになります。
uv run pytest -m "not integration and not e2e" -v
これは動作としては可能ですが、コマンドだけ見たときに 「unit テストを実行している」ことが分かりづらいです。
2. ディレクトリ構成自体がテスト分類になる
tests/unit に置かれていれば unit テスト、tests/e2e に置かれていれば e2e テストです。
つまり、配置場所そのものが分類情報になるため、レビュー時にも把握しやすくなります。
たとえば tests/unit に Databricks Connect を使う重いテストが紛れ込んだ場合、ディレクトリだけでも違和感に気づきやすくなります。
3. unit を「消極的定義」にしなくて済む
マーカー中心の運用だと、unit テストは次のように定義されがちです。
- integration でも e2e でもないもの = unit
これは unit が 「残り物のカテゴリ」 になりやすいです。
一方で、ディレクトリで tests/unit を用意しておけば、unit を明示的に扱えます。
マーカーは不要なのか
不要ではありません。
ディレクトリで実行制御していても、integration / e2e マーカーは残す価値があります。
理由1: テストの意味がコード上で分かる
たとえば integration テストに次のようなマーカーが付いていると、テストを単体で見たときにも意味が分かります。
import pytest
@pytest.mark.integration
def test_read_table():
...
ファイル単体を読んだときにも、**「これは Databricks Connect を使う統合テストだな」**と分かります。
理由2: --strict-markers で誤字を防げる
今回の設定では、未登録マーカーを使うとエラーになります。
[tool.pytest.ini_options]
addopts = "--strict-markers"
markers = [
"integration: Databricks Connect経由でServerless computeに接続する統合テスト",
"e2e: Databricks Asset Bundles経由でジョブ/パイプラインを実行するE2Eテスト",
]
たとえば次のような typo はエラーになります。
import pytest
@pytest.mark.integrtion # 誤字
def test_x():
...
マーカーは実行制御だけでなく、分類ラベルの品質保証にも役立ちます。
理由3: 将来的に -m で柔軟に絞り込める
今は CI をディレクトリで回していても、将来的にこんな要件が出ることがあります。
- integration の中でも重いテストだけ除外したい
- e2e の中でも一部だけ回したい
- nightly 実行だけ対象を増やしたい
そういうとき、マーカーがあると -m による柔軟な抽出ができます。
実装例
pytest の設定
[tool.pytest.ini_options]
# markerの誤字を検知するため、未登録markerはエラーにする
addopts = "--strict-markers"
markers = [
"integration: Databricks Connect経由でServerless computeに接続する統合テスト",
"e2e: Databricks Asset Bundles経由でジョブ/パイプラインを実行するE2Eテスト",
]
テストディレクトリ構成
tests/
├─ unit/
├─ integration/
└─ e2e/
integration テストの例
import pytest
@pytest.mark.integration
def test_read_table():
# Databricks Connect を使う統合テスト
...
e2e テストの例
import pytest
@pytest.mark.e2e
def test_pipeline_run():
# Databricks Asset Bundles 経由でジョブ/パイプラインを実行するE2Eテスト
...
CI での実行例
unit テスト
uv run pytest tests/unit -v
integration テスト
uv run pytest tests/integration -v
e2e テスト
uv run pytest tests/e2e -v
このように、CI ではディレクトリで分けると意図が分かりやすくなります。
pytest -m を使うとどうなるか
マーカーを使うと、次のような絞り込みができます。
integration だけ実行
pytest -m integration
e2e だけ実行
pytest -m e2e
integration と e2e を除いたテストだけ実行
pytest -m "not integration and not e2e"
ただし、今回のようにディレクトリ構成がきれいに分かれている場合、CI の主制御としてはディレクトリの方が分かりやすいです。
-m は補助的に使うのが扱いやすいです。
ハマりどころ
1. マーカーを付けただけでは通常実行から除外されない
たとえば次のようなテストがあるとします。
import pytest
@pytest.mark.e2e
def test_pipeline():
...
この状態で pytest を実行すると、e2e テストも普通に実行されます。
マーカーは「分類ラベル」であり、付けただけでは自動で除外されません。
除外したい場合は、-m で条件を指定する必要があります。
pytest -m "not e2e"
2. pytest -m integration は unit テストを含まない
pytest -m integration
は integration マーカーが付いたテストだけを実行します。
unit テストが一緒に実行されるわけではありません。
そのため、CI で「unit テストを実行する」目的で -m を使うと、式がやや読みにくくなりがちです。
pytest -m "not integration and not e2e"
この点も、unit テストの主制御をディレクトリに寄せる理由になります。
3. ディレクトリとマーカーがズレると運用が崩れやすい
たとえば tests/integration にあるのに @pytest.mark.integration が付いていない、あるいは tests/unit に @pytest.mark.e2e が付いている、といった状態は避けたいです。
ルールとして次を決めておくと運用しやすくなります。
-
tests/integration/**のテストには@pytest.mark.integrationを付ける -
tests/e2e/**のテストには@pytest.mark.e2eを付ける -
tests/unit/**にはintegration/e2eを付けない
まとめ
-
tests/unit,tests/integration,tests/e2eのようにディレクトリが分かれているなら、CI の主制御はディレクトリで行うのが分かりやすいです -
integration/e2eマーカーは削らず、意味付けと保険として残すのが有効です -
--strict-markersを使うと、マーカーの誤字や未登録マーカーを検知できます -
pytest -m ...は便利ですが、今回のような構成では CI の主制御より補助用途に向いています - 役割分担としては、実行制御はディレクトリ、分類と柔軟な抽出はマーカーに寄せると整理しやすいです