pytest on Ubuntu 応用 — フィクスチャ・モック・CI統合の実践

開発環境

この記事のポイント

  • conftest.py にフィクスチャをまとめると複数テストファイルから共有できる
  • @pytest.fixture(params=[...]) でパラメータ化フィクスチャを使うと、DBバックエンドや環境ごとのテストを自動展開できる
  • mocker.patch()"モジュール名.関数名" のパスで指定する——from app import func した参照は別オブジェクトになるので直接パッチできない
  • coverage run -m pytestcoverage report --show-missing で未テスト行を特定できる
  • 本記事は Ubuntu 24.04 LTS(Docker公式イメージ)で実際に実行した結果を掲載しています

pytest の基礎(インストール・基本的なテスト実行)はすでに使えるとして、今回はフィクスチャ・モック・カバレッジ計測の3本柱を掘り下げます。

「外部APIを呼び出すコードをどうテストするか」「同じセットアップコードをテスト間で使い回したい」——実際の開発で壁にぶつかる問題です。Ubuntu 24.04 LTS の Docker 環境で全コマンドを実行し、出てきた実出力をそのまま掲載します。

動作確認済み環境

Ubuntu 24.04.4 LTS(docker run –rm ubuntu:24.04)/ Python 3.12.3 / pytest 9.1.1 / pytest-mock 3.15.1 / coverage 7.14.2(2026-06-22 実測)

目次

  1. インストール:pytest + pytest-mock + coverage
  2. フィクスチャの基本とconftest.py
  3. パラメータ化フィクスチャで複数環境を自動展開
  4. pytest-mock でモックを使う
  5. coverage でカバレッジを計測する
  6. バージョン別の違い(Ubuntu 22.04 vs 24.04)
  7. よくあるエラーと解決策
  8. まとめ

インストール:pytest + pytest-mock + coverage

Ubuntu には venv(仮想環境)を使ってインストールします。python3-venv パッケージが必要なので、先にインストールしておきます。




ubuntu@linuxlab: ~/myproject
$ sudo apt update && sudo apt install python3-venv -y
Reading package lists… Done
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install pytest pytest-mock coverage
Successfully installed coverage-7.14.2 pytest-9.1.1 pytest-mock-3.15.1 …
$ pytest –version
pytest 9.1.1

Docker 環境で実際に実行した結果がこちらです。

pytest 9.1.1 + pytest-mock 3.15.1 + coverage 7.14.2 インストールログ(ubuntu:24.04 実測)
pytest 9.1.1 + pytest-mock 3.15.1 + coverage 7.14.2 インストールログ(ubuntu:24.04 実測)

pytest-mock は pytest のプラグインで、テスト内で mocker フィクスチャを使えるようにするパッケージです。coverage は別パッケージとして提供されていますが、pip install 1行で全部入ります。

フィクスチャの基本とconftest.py

フィクスチャとは、テストに渡す「お膳立て済みのオブジェクト」を用意する仕組みです。@pytest.fixture デコレータを付けた関数がそれにあたります。

①基本的なフィクスチャを書く

テストファイルと同じ場所に conftest.py を作ると、そのディレクトリ以下の全テストから自動で読み込まれます。




conftest.py
import pytest

@pytest.fixture
def sample_user():
return {“id”: 1, “name”: “Alice”, “email”: “alice@example.com”, “active”: True}

@pytest.fixture
def inactive_user():
return {“id”: 2, “name”: “Bob”, “email”: “bob@example.com”, “active”: False}

テスト関数の引数にフィクスチャ名を書くだけで、pytest が自動的に注入してくれます。




test_user.py
def test_user_name(sample_user):
assert sample_user[“name”] == “Alice”

def test_user_active(sample_user):
assert sample_user[“active”] is True

class TestUserEmail:
def test_email_format(self, sample_user):
assert “@” in sample_user[“email”]

クラスの中でもフィクスチャは同じように使えます。self の次の引数として書くだけです。

②フィクスチャのスコープ

デフォルトでは、フィクスチャはテストごとに1回呼ばれます。DBコネクションのように「セッション中に1回だけ確立したい」場合は scope を指定します。




conftest.py
@pytest.fixture(scope=”session”)
def db_connection():
conn = create_connection() # セッション全体で1回だけ
yield conn
conn.close() # テスト終了後にクリーンアップ

スコープは function(デフォルト)・classmodulesession の4段階です。yield を使うと前処理と後処理を1つのフィクスチャに書けます——ここが最初に詰まったポイントでした。return ではなく yield にすると、yield 以降がテスト終了後に実行されます。

パラメータ化フィクスチャで複数環境を自動展開

「SQLiteとPostgreSQLの両方でテストを通したい」というケースで効果を発揮するのがパラメータ化フィクスチャです。




conftest.py
@pytest.fixture(params=[“sqlite”, “postgresql”])
def db_backend(request):
return request.param



test_user.py
def test_db_backend_available(db_backend):
assert db_backend in (“sqlite”, “postgresql”)

これだけで、pytest が自動的に [sqlite][postgresql] の2パターンを作って実行します。

フィクスチャ実行ログ。7テスト全PASS・db_backendがsqlite/postgresqlに自動展開(ubuntu:24.04 実測)
フィクスチャ実行ログ。7テスト全PASS・db_backendがsqlite/postgresqlに自動展開(ubuntu:24.04 実測)

実測では7テストが0.01秒で全PASSしました。test_db_backend_available[sqlite]test_db_backend_available[postgresql] が別テストとして実行されているのが確認できます。テストを追加するたびにパラメータの数だけ自動展開されるので、環境ごとの書き漏らしを防げます。

正直、最初は @pytest.mark.parametrize と混同していました。@pytest.mark.parametrize はテスト関数に直接書くもので、フィクスチャに params を渡すのとは別物です。どちらも「パラメータを増やす」という意図は同じですが、フィクスチャ側に書くと複数のテスト関数にまとめて適用できるのが違いです。

pytest-mock でモックを使う

外部API・データベース・SMTPサーバーなど、テスト環境で実際に呼び出せないものをテストするのに使います。mocker フィクスチャは pytest-mock が自動で提供してくれます。

①基本的なpatch

テスト対象の app.py は外部APIを呼び出します。




app.py
import requests

def fetch_user_status(user_id: int) -> str:
response = requests.get(f”https://api.example.com/users/{user_id}”)
data = response.json()
return data.get(“status”, “unknown”)

テストでは実際にHTTPリクエストを飛ばさず、mocker.patch() で差し替えます。




test_app.py
import app
from unittest.mock import MagicMock

def test_fetch_user_status_active(mocker):
mock_response = MagicMock()
mock_response.json.return_value = {“status”: “active”, “id”: 1}
mocker.patch(“app.requests.get”, return_value=mock_response)

result = app.fetch_user_status(1)
assert result == “active”

よく踏む落とし穴:patchのパスはモジュール参照を指定する

from app import fetch_user_status とインポートしてしまうと、テスト側は fetch_user_status への参照を直接持ちます。この場合 mocker.patch("app.requests.get") は機能しますが、もし app.requests ではなく requests.get とパッチしてしまうと app.py の呼び出しには反映されません。パッチ先は「その関数が使われる名前空間」で指定するのが鉄則です。

②call_countで呼び出し回数を検証する




test_app.py
def test_mock_call_count(mocker):
mock_response = MagicMock()
mock_response.json.return_value = {“status”: “active”}
mock_get = mocker.patch(“app.requests.get”, return_value=mock_response)

app.fetch_user_status(1)
app.fetch_user_status(2)

assert mock_get.call_count == 2

③side_effectで呼び出しごとに異なる値を返す

1回目と2回目で異なるレスポンスを返させたい場合は side_effect を使います。




test_app.py
def test_mock_with_side_effect(mocker):
mock_res1 = MagicMock()
mock_res1.json.return_value = {“status”: “active”}
mock_res2 = MagicMock()
mock_res2.json.return_value = {“status”: “inactive”}
mocker.patch(“app.requests.get”, side_effect=[mock_res1, mock_res2])

assert app.fetch_user_status(1) == “active”
assert app.fetch_user_status(2) == “inactive”

実際に動かすと4テスト全てPASSします。

pytest-mock 実行ログ。mocker.patch/side_effect 4テスト全PASS(ubuntu:24.04 実測)
pytest-mock 実行ログ。mocker.patch/side_effect 4テスト全PASS(ubuntu:24.04 実測)

coverage でカバレッジを計測する

テストが通っても、コードの全行を実行しているとは限りません。coverage を使うと「どの行がテストで実行されたか」を可視化できます。

①coverage run でテストを計測する




ubuntu@linuxlab: ~/myproject
$ coverage run -m pytest -v test_calculator.py
… 5 passed in 0.01s
$ coverage report –show-missing
Name Stmts Miss Cover Missing
————————————————–
calculator.py 12 0 100%
test_calculator.py 16 0 100%
————————————————–
TOTAL 28 0 100%
coverage report 実行ログ。calculator.py 100%カバレッジ達成(ubuntu:24.04 実測)
coverage report 実行ログ。calculator.py 100%カバレッジ達成(ubuntu:24.04 実測)

実測では calculator.py の全12ステートメントをカバーできました。pytest.raises で例外分岐(ゼロ除算)も通しているため、分岐行も100%に含まれます。

②HTMLレポートで視覚的に確認する




ubuntu@linuxlab: ~/myproject
$ coverage html
Wrote HTML report to htmlcov/index.html
$ python3 -m http.server 8080 –directory htmlcov
Serving HTTP on 0.0.0.0 port 8080 …

ブラウザで http://localhost:8080 を開くと、各ソースファイルの行ごとにカバレッジが色付きで確認できます。未実行行は赤くハイライトされるので、テストが届いていない分岐がひと目でわかります。

③pytestと組み合わせるなら pytest-cov が便利




ubuntu@linuxlab: ~/myproject
$ pip install pytest-cov
$ pytest –cov=. –cov-report=term-missing
———- coverage: platform linux, python 3.12.3 ———-
Name Stmts Miss Cover Missing
calculator.py 12 0 100%

pytest-cov を使うと coverage runcoverage report を1コマンドにまとめられます。CI環境では --cov-fail-under=80 オプションを追加して、カバレッジが80%未満なら CI を失敗させる設定が一般的です。

バージョン別の違い(Ubuntu 22.04 vs 24.04)

Docker で Ubuntu 22.04 と 24.04 の両方を実際に動かして確認した結果です。

Ubuntu 22.04 vs 24.04 でのpytest/Python バージョン比較(実測)
Ubuntu 22.04 vs 24.04 でのpytest/Python バージョン比較(実測)

pytest・pytest-mock・coverage のバージョンは pip でインストールする場合は同じ最新版が入りますが、Python 本体は 22.04 が 3.10.12・24.04 が 3.12.3 と2世代の差があります。Python 3.11 以降ではエラーメッセージが大幅に改善されているので、VPS やサーバーを新規に立てるなら Ubuntu 24.04 を選ぶ方が長い目で見て得です。

GitHub Actions でCI統合する

ローカルで通ったテストをプッシュ時にも自動実行するのが CI です。GitHub Actions を使う場合、 .github/workflows/test.yml を以下のように書きます。




.github/workflows/test.yml
name: pytest
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-24.04
steps:
– uses: actions/checkout@v4
– uses: actions/setup-python@v5
with:
python-version: “3.12”
– run: pip install pytest pytest-mock pytest-cov
– run: pytest –cov=. –cov-fail-under=80

runs-on: ubuntu-24.04 にすることでローカルの Ubuntu 24.04 環境と揃えられます。--cov-fail-under=80 でカバレッジが80%を下回るとCIが赤くなる設定です。この数値は最初は低めに設定して、テストを増やしながら段階的に上げていくのが実際のプロジェクトでの使い方です。

よくあるエラーと解決策

①ModuleNotFoundError: No module named ‘pytest’




ubuntu@linuxlab: ~
ModuleNotFoundError: No module named ‘pytest’

仮想環境が有効になっていない状態で実行しているケースがほとんどです。source .venv/bin/activate を実行してから pytest を叩いてください。

②AssertionError: Expected ‘xxx’ to be called once. Called 0 times.




ubuntu@linuxlab: ~
AssertionError: Expected ‘send_notification’ to be called once. Called 0 times.

from app import send_notification でインポートした後に mocker.patch("app.send_notification") してしまうと、テストコードが持つ参照はパッチ前のオブジェクトのままです。解決策は import app して app.send_notification(...) と呼ぶか、mocker.patch("test_app.send_notification") と呼び出し側の名前空間でパッチすることです。

③fixture ‘mocker’ not found




ubuntu@linuxlab: ~
fixture ‘mocker’ not found

pytest-mock がインストールされていません。pip install pytest-mock で解決します。

④no data was collected(coverage)




ubuntu@linuxlab: ~
CoverageWarning: No data was collected.

coverage run pytest-m なし)としているか、テストファイルが見つかっていないケースです。coverage run -m pytest-m を付けてください。

まとめ

Ubuntu 24.04 LTS(Docker)で pytest 9.1.1 + pytest-mock 3.15.1 + coverage 7.14.2 を実際に動かして確認した内容をまとめます。

  • conftest.py にフィクスチャを書くと、そのディレクトリ以下の全テストから自動で読み込まれる
  • @pytest.fixture(params=[...]) を使うとパラメータの数だけテストが自動展開される
  • mocker.patch("app.requests.get") は「その関数が使われる名前空間」でパッチする——from app import func した参照は別オブジェクトなので引っかかりやすい
  • side_effect に配列を渡すと呼び出し順に異なる戻り値を返せる
  • coverage run -m pytest && coverage report --show-missing で未テスト行を特定できる
  • GitHub Actions に組み込む場合は --cov-fail-under=80 でカバレッジの下限を設けるのが実用的

テストが書けるようになると、コードを変えるときの「動いていたものを壊していないか」という不安がかなり減ります。VPS でサーバーを運用するつもりなら、デプロイ前に CI でテストを通す習慣を早めに作るのがおすすめです。

VPS の選び方や初期セットアップは https://linuxlab.jp/ubuntu-server-setup/ にまとめています。

コメント

タイトルとURLをコピーしました