# Flaskのテスト実践ガイド：pytest fixtures・test_client・test_cli_runnerで本番品質の自動テストを書く

> Flask 3.1系のテストを本番品質で書く完全ガイド。アプリケーションファクトリとtest_clientがなぜテストを容易にするか、TESTING=Trueの効果、app/client/runnerのpytest fixture三点セット、response.data/json/text・follow_redirects・session_transactionによるリクエスト検証、test_request_contextとapp_context、test_cli_runnerでのCLIテストまで、公式ドキュメントに忠実な実コードで解説します。

- 公開日: 2026-06-23
- 著者: 友田 陽大
- タグ: Python, Flask, pytest, テスト, バックエンド, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/flask-testing-pytest-test-client-fixtures-guide

## 要点

- Flaskがテストしやすいのはアプリケーションファクトリとtest_client()のおかげ。実サーバーを立てずにリクエストを往復でき、テスト専用設定のアプリを毎回作れる
- fixtureはapp/client/runnerの三点セットが正準。pytestはfixture名と引数名を一致させて注入し、yield後のコードがテスト後の後始末（DB破棄など）を担う
- response.data（bytes）・response.json・response.textで本文を検証し、follow_redirectsとresponse.historyでリダイレクトを追う。Location検証はステータスとヘッダで行う
- session/gの検証はwith client:でリクエスト後に読み、ログインの前提はclient.session_transaction()でセッションを先に仕込む
- test_request_contextはrequestを読む関数の単体テストに、app_contextはDB層のテストに使う。CLIコマンドはtest_cli_runner＋monkeypatchで検証する

---

## **導入：Flask のテストは「設計のご褒美」である**

テストが書きづらいバックエンドには、ほぼ例外なく**設計の歪み**があります。グローバルな `app` をモジュールトップに置いていて設定を差し替えられない、DB 接続がハードコードされていてテスト用 DB に向けられない、ビュー関数にビジネスロジックが癒着していて単体で叩けない——こうした「テストのしづらさ」は、そのまま「変更のしづらさ」です。

逆に言えば、**テストが素直に書けるかどうかは、その Flask アプリが本番品質かどうかのリトマス試験紙**になります。そして Flask は、[ピラー記事](/blog/flask-production-guide)で解説したアプリケーションファクトリ（`create_app`）を採用していれば、テストが驚くほど書きやすいフレームワークです。実サーバーを立てずにリクエストを往復できる `test_client()`、テスト専用設定のアプリを毎回作れるファクトリ、そして pytest の fixture——この 3 つが噛み合うと、「境界の契約をテストで固定する」設計が最小の労力で実現します。

本記事は、ピラーの §9 を専用記事として深掘りするスポークです。筆者は、**経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python / Flask / SQLAlchemy / PostgreSQL で設計・実装し、API Gateway → ALB → ECS(Fargate) 上で本番運用**してきました。ここで示すのは、その実戦でリグレッションを防ぎ続けたテストの型です。

> 💡 **この記事で扱うバージョン**：**Flask 3.1 系**（最小 Python 3.9）と **pytest** を前提とします。コードはすべて公式ドキュメント（[flask.palletsprojects.com](https://flask.palletsprojects.com/en/stable/testing/)）のテストガイドおよびチュートリアルのパターンに基づきます。E2E（ブラウザを介したフロント込みの動線テスト）は対象外で、その層は対になる [Playwright E2E テスト設計ガイド](/blog/playwright-e2e-testing-production-design-guide) に分けています。本記事は **Flask 側のサーバーテスト**に集中します。

---

## **1. なぜ Flask のテストは容易なのか：`test_client` と `TESTING=True`**

### 1.1 `test_client()`：サーバーを立てずにリクエストを往復する

Flask アプリは WSGI アプリケーションであり、Werkzeug の上に乗っています。この性質を利用して、Flask は **実際の HTTP サーバーを起動せずにアプリへリクエストを送れるテストクライアント**を提供します。

```python
client = app.test_client()
response = client.get("/health")
```

`test_client()` は、ネットワークも開発サーバーも介さずに、WSGI レベルで直接アプリのディスパッチを駆動します。だから速く、ポート競合もなく、CI でも安定します。`use_cookies=True`（既定）なら Cookie をリクエスト間で保持するので、ログイン状態を引き継いだ複数リクエストのフローも書けます。

### 1.2 `TESTING=True` が何を変えるのか

テストを書く前に、必ず **`app.testing = True`（または設定 `TESTING=True`）を立てます**。公式の言葉を借りれば「`TESTING` は Flask にアプリがテストモードであることを伝え、Flask はいくつかの内部挙動を変えてテストを容易にする」。具体的に重要なのは次の点です。

- **例外を握りつぶさず伝播させる**。通常、ビュー内で発生した例外は Flask がエラーハンドラで捕まえて 500 レスポンスに変換します。テストモードでは、これがテスト側に**例外として伝播**するため、「なぜか 500 が返る」を**スタックトレースで**追えます。これがないと、アサーションが落ちた本当の原因が見えません。

```python
def test_config():
    # ファクトリにTESTINGを渡さなければtestingはFalse
    assert not create_app().testing
    # 渡せばTrueになる——設定が効いていることをまず固定する
    assert create_app({"TESTING": True}).testing
```

> ⚠️ **`TESTING` を立て忘れる罠**：`TESTING=True` を設定し忘れると、ビューで起きた本物のバグ（属性エラー・型エラー）が 500 レスポンスに化けてしまい、テストは「ステータスが 200 ではない」としか教えてくれません。デバッグに無駄な時間を溶かす典型です。**fixture でアプリを作る時点で必ず `TESTING=True` を渡す**ことを、テスト設計の最初の規律にしてください。

---

## **2. fixture 三点セット：`app` / `client` / `runner`**

### 2.1 正準形：名前で注入される 3 つの fixture

pytest は、**テスト関数の引数名と一致する名前の fixture を探して自動注入**します。Flask のテストでは、この仕組みに乗せた `app` / `client` / `runner` の三点セットが正準です。これを `tests/conftest.py` に置くと、配下のすべてのテストファイルから引数名だけで使えます。

```python
# tests/conftest.py
import pytest

from my_project import create_app


@pytest.fixture()
def app():
    app = create_app()
    app.config.update({"TESTING": True})

    # ここに setup 処理（DB作成など）を書ける
    yield app
    # yield の後ろが teardown（後始末）。ここに破棄処理を書く


@pytest.fixture()
def client(app):
    return app.test_client()


@pytest.fixture()
def runner(app):
    return app.test_cli_runner()
```

それぞれの役割を整理します。

| fixture | 生成物 | 何をテストするか |
|---|---|---|
| `app` | `create_app({"TESTING": True})` で作ったアプリ | アプリそのもの・設定・`app_context` を要する DB 層 |
| `client` | `app.test_client()` | HTTP リクエスト/レスポンスの往復（ビュー・ルーティング） |
| `runner` | `app.test_cli_runner()` | `@app.cli.command` で定義した CLI コマンド |

`client` と `runner` が `app` を引数に取っている点に注目してください。pytest は「`client` を使うテストには、まず `app` fixture を解決してから渡す」という**依存解決**を自動でやってくれます。テストごとに新鮮な `app` が作られ、テスト間で状態が漏れません。

> 💡 **なぜファクトリでないとこれが書けないのか**：fixture の `create_app({"TESTING": True})` という一行こそが、アプリケーションファクトリの最大の配当です。グローバル `app` をモジュールトップに置く設計では、import 時にアプリが確定してしまい、テスト用設定を差し込む隙がありません。「テスト容易性」は将来の要件ではなく**現在の要件**であり、ファクトリはそれを満たすための分離です。詳細は [大規模構成ガイド](/blog/flask-application-factory-blueprints-large-app-structure-guide) を参照してください。

### 2.2 一時 DB 版：setup と teardown を fixture に閉じ込める

実アプリのテストは DB を伴います。公式チュートリアルの conftest は、**テストごとに一時ファイルの SQLite を作り、テスト後に確実に消す**パターンを示しています。`yield` の前が setup、後ろが teardown という pytest fixture の構造が、リソースのライフサイクルをきれいに閉じ込めます。

```python
import os
import tempfile

import pytest

from flaskr import create_app
from flaskr.db import get_db, init_db

# テスト用の初期データSQLをあらかじめ読み込んでおく
with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f:
    _data_sql = f.read().decode("utf8")


@pytest.fixture
def app():
    # 一時ファイルを作り、そのパスをDATABASE設定に渡す
    db_fd, db_path = tempfile.mkstemp()

    app = create_app({"TESTING": True, "DATABASE": db_path})

    with app.app_context():
        init_db()                                  # スキーマを作る
        get_db().executescript(_data_sql)          # 初期データを投入

    yield app

    # teardown：一時DBを閉じて削除する。テスト間で状態を持ち越さない
    os.close(db_fd)
    os.unlink(db_path)
```

この fixture の `with app.app_context():` は重要です。`init_db()` や `get_db()` は `current_app` を参照するため、アプリケーションコンテキストの中でないと `RuntimeError: Working outside of application context` で落ちます（コンテキストの詳細は [コンテキスト徹底解説](/blog/flask-application-request-context-g-current-app-guide) を参照）。fixture でコンテキストを push しておくことで、DB 初期化を安全に実行できます。

> 💡 **fixture のスコープと「クリーンさ」のトレードオフ**：上の fixture は既定の `function` スコープで、**テスト関数ごとに DB を作り直す**ため最もクリーンですが、件数が増えると遅くなります。高速化したい場合は `@pytest.fixture(scope="session")` で DB を作る回数を減らし、各テストはトランザクションをロールバックして隔離する、という設計もあります。ただし「速さ」のために「テスト間の独立性」を犠牲にすると、テストの順序依存という最悪のフレークを生みます。まずは `function` スコープで正しさを担保し、遅さが実測で問題になってから最適化してください——推測で最適化しないのが鉄則です。

---

## **3. リクエストテスト：`response.data` / `json` / `text` を使い分ける**

### 3.1 GET と本文の検証

`client.get(path)` がレスポンスオブジェクトを返します。本文は、期待する形式に応じて 3 つのプロパティを使い分けます。

```python
def test_hello(client):
    response = client.get("/hello")
    # response.data は bytes。バイト列リテラル（b"...")と比較する
    assert response.data == b"Hello, World!"
```

| アクセサ | 型 | 用途 |
|---|---|---|
| `response.data` | `bytes` | 生のレスポンスボディ（バイト列） |
| `response.json` | `dict` / `list` | JSON レスポンスをパースした Python オブジェクト |
| `response.text` | `str` | テキストとしてデコードした本文（`get_data(as_text=True)` と同義） |
| `response.status_code` | `int` | ステータスコード |
| `response.headers` | dict 風 | レスポンスヘッダ（`Location` 等） |

REST API のテストでは `response.json` が主役です。`jsonify` で返したレスポンスをパース済みの dict として受け取れるため、本文の構造をそのまま辞書比較できます。

```python
def test_health_returns_json(client):
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json == {"status": "ok"}      # パース済みdictを直接比較
```

### 3.2 POST・ヘッダ・`Location` の検証

POST はフォーム送信なら `data=`、JSON ボディなら `json=` で渡します。リダイレクトを返すエンドポイントでは、**ステータスと `Location` ヘッダ**を検証します。

```python
def test_register(client, app):
    # GETでフォーム画面が出ることを確認
    assert client.get("/auth/register").status_code == 200

    # POSTで登録 → 成功するとログイン画面へリダイレクトする
    response = client.post(
        "/auth/register", data={"username": "a", "password": "a"}
    )
    # リダイレクト先はLocationヘッダで検証する
    assert response.headers["Location"] == "/auth/login"

    # 副作用（DBに行が作られたか）はapp_contextの中で直接確認する
    with app.app_context():
        assert (
            get_db()
            .execute("SELECT * FROM user WHERE username = 'a'")
            .fetchone()
            is not None
        )
```

このテストが示す本質は、**「HTTP の応答（Location）」と「副作用（DB の状態）」を両方検証する**ことです。リダイレクト先が正しくても DB に書けていなければ機能していませんし、その逆もまた然り。`client` でリクエストを往復しつつ、`app.app_context()` で DB を直接覗くこの組み合わせが、登録系エンドポイントのテストの型です。

### 3.3 `follow_redirects`：リダイレクトを追う

リダイレクト先まで含めて検証したいときは、`follow_redirects=True` を渡すと**最終ページまで追跡**します。途中の遷移は `response.history` に、最終的に到達した URL は `response.request.path` に入ります。

```python
def test_logout_redirects_to_index(client):
    response = client.get("/logout", follow_redirects=True)
    # 1回リダイレクトが起きたことを確認
    assert len(response.history) == 1
    # 最終的にトップへ着地したことを確認
    assert response.request.path == "/"
```

> 💡 **`Location` 検証か `follow_redirects` か**：「どこへ飛ばすか」だけを契約として固定したいなら `follow_redirects` を使わず、`response.headers["Location"]` を直接アサートするのが軽量で意図も明確です（3.2 の例）。一方、「飛んだ先のページが正しく描画されるか」まで見たいなら `follow_redirects=True` で追跡します。両者は**検証している契約が違う**ので、テストの意図に合わせて選んでください。

---

## **4. セッションのテスト：読むときと仕込むときで道具が違う**

ログインを伴うアプリでは、`session` の検証が避けて通れません。Flask は「**リクエスト後にセッションを読む**」場合と「**リクエスト前にセッションを仕込む**」場合とで、別々の道具を用意しています。ここを混同すると `RuntimeError` で詰まります。

### 4.1 読む：`with client:` でリクエスト後に `session` / `g` を覗く

通常、`session` や `g` はリクエストの外では触れません（コンテキストが無いため）。`with client:` ブロックの中でリクエストを送ると、**そのリクエストのコンテキストがブロック終了まで保持され**、リクエスト後に `session` を読めます。

```python
from flask import session


def test_access_session(client):
    with client:
        client.post("/auth/login", data={"username": "flask"})
        # ログイン直後、サーバーがセッションに書いた値を検証できる
        assert session["user_id"] == 1
    # with ブロックを抜けると session はもうアクセスできない
```

ログイン処理が `session["user_id"]` を正しくセットしたか——という**サーバー内部の副作用**を、レスポンス本文を介さず直接確認できるのがこの手法の価値です。`g` に積んだ値も同様に覗けます。

### 4.2 仕込む：`client.session_transaction()` でリクエスト前にセッションを書く

逆に、「すでにログイン済みのユーザー」を前提としたエンドポイント（例：`/users/me`）をテストしたいときは、**リクエストの前にセッションを仕込みます**。`client.session_transaction()` のブロック内でセッションを書くと、**ブロック終了時に署名付き Cookie として保存**され、以降のリクエストに引き継がれます。

```python
def test_users_me_with_logged_in_user(client):
    # ログインフローを毎回叩く代わりに、セッションを直接仕込む
    with client.session_transaction() as session:
        session["user_id"] = 1

    response = client.get("/users/me")
    assert response.status_code == 200
    assert response.json["id"] == 1
```

これは**ログインのショートカット**として極めて有用です。認証が必要な 20 個のエンドポイントをテストするのに、毎回ログイン POST を往復させるのは遅く・冗長です。`session_transaction()` でセッションを直接組み立てれば、「認証済み」という前提を 3 行で用意できます。

### 4.3 再利用：認証済み client を fixture にする

このパターンは fixture に切り出すと、認証テスト全体が劇的に読みやすくなります。

```python
# tests/conftest.py（追加）
@pytest.fixture()
def auth_client(client):
    """user_id=1 でログイン済みの client を返す。"""
    with client.session_transaction() as session:
        session["user_id"] = 1
    return client
```

```python
# 認証が要るエンドポイントは auth_client を引数に取るだけ
def test_dashboard_requires_auth(client):
    # 未ログインは弾かれる（リダイレクトや401）
    assert client.get("/dashboard").status_code in (302, 401)


def test_dashboard_shows_for_authed_user(auth_client):
    # ログイン済み前提でビジネスロジックを検証する
    assert auth_client.get("/dashboard").status_code == 200
```

「未認証は弾く・認証済みは通す」というアクセス制御の契約が、fixture を切り替えるだけで宣言的に書けます。これは認可（Authorization）のリグレッションを防ぐ、本番品質のテストの要です。

---

## **5. `test_request_context`：`request` を読む関数を単体テストする**

ビュー関数の中から呼ばれる**ヘルパー関数**が `request`（フォーム値・クエリ・JSON）を直接参照していることはよくあります。こうした関数を、HTTP の完全なディスパッチを介さずに単体テストしたいときに使うのが `app.test_request_context()` です。

`test_request_context()` は Werkzeug の `EnvironBuilder` の引数（`path` / `method` / `data` / `json` / `query_string` / `headers` …）を受け取り、**リクエストコンテキストだけを擬似的に作って**そのブロック内で `request` を使えるようにします。

```python
def test_validate_user_edit(app):
    # /user/2/edit に空のnameをPOSTした状況を擬似的に作る
    with app.test_request_context(
        "/user/2/edit", method="POST", data={"name": ""}
    ):
        # validate_edit_user() は内部で request.form を読む
        messages = validate_edit_user()

    assert messages["name"][0] == "Name cannot be empty."
```

ビューを丸ごと叩く（`client.post`）よりも、検証ロジックだけを**狙い撃ちで・高速に**テストできます。境界バリデーションの単体テストとして相性が良く、原因の切り分けも容易です（境界をスキーマで設計する考え方は [marshmallow × Flask × SQLAlchemy の境界設計ガイド](/blog/marshmallow-flask-sqlalchemy-rest-api-production-guide) と一貫します——境界の契約をテストで固定する、という同じ思想です）。

> ⚠️ **`before_request` は呼ばれない**：`test_request_context()` は**ディスパッチコードを実行しない**ため、`@app.before_request` で登録した前処理（認証チェック・`g` への値の積み込みなど）は**走りません**。テスト対象の関数がそれらの前提に依存している場合は、コンテキスト内で明示的に `app.preprocess_request()` を呼んでください。

```python
def test_handler_that_depends_on_before_request(app):
    with app.test_request_context("/orders", json={"qty": 3}):
        app.preprocess_request()   # before_request を手動で起動する
        result = handle_create_order()
    assert result["qty"] == 3
```

`client` 経由のテストでは `before_request` は当然走るので、この注意は **`test_request_context` を使う単体テスト特有**のものです。完全なリクエストライフサイクルを再現したいなら `client` を、ロジックだけを狙い撃ちしたいなら `test_request_context` を選ぶ、という使い分けになります。

---

## **6. `app_context`：リクエストに紐づかない DB 層をテストする**

`request` を一切使わない、純粋な**データアクセス層**（`get_db()` や ORM のクエリ関数）をテストしたいことがあります。これらは `current_app` を参照するので**アプリケーションコンテキスト**が必要ですが、**リクエストコンテキストは不要**です。そのための道具が `app.app_context()` です。

```python
def test_get_db_returns_same_connection(app):
    # リクエストは無いが、current_app が要るので app_context を push
    with app.app_context():
        db = get_db()
        # 同一コンテキスト内では同じ接続が再利用される（g にキャッシュ）
        assert db is get_db()


def test_close_db_after_context(app):
    with app.app_context():
        db = get_db()

    # コンテキストを抜けると teardown_appcontext で接続が閉じられる
    import pytest
    with pytest.raises(Exception):
        db.execute("SELECT 1")   # 閉じた接続を使うと例外になる
```

`test_request_context`（リクエスト＋アプリ両方のコンテキスト）と `app_context`（アプリのコンテキストのみ）の使い分けを整理します。

| 道具 | 押し込まれるコンテキスト | `request` が使えるか | 主な用途 |
|---|---|---|---|
| `client.get(...)` | リクエスト＋アプリ（フルディスパッチ） | 使える | ビュー・ルーティング・E2E 的な往復 |
| `app.test_request_context()` | リクエスト＋アプリ（ディスパッチなし） | 使える | `request` を読むヘルパー関数の単体テスト |
| `app.app_context()` | アプリのみ | 使えない | `current_app` だけ要る DB 層・CLI 的処理 |

「テスト対象が何のコンテキストを必要とするか」で道具を選ぶ——これが Flask テストの設計判断の核です。

---

## **7. CLI コマンドのテスト：`test_cli_runner` + `monkeypatch`**

Flask アプリには、`@app.cli.command` で定義する**管理コマンド**（DB 初期化・データ移行・バッチ）が付き物です。これらも `app.test_cli_runner()`（Click の `CliRunner` を Flask 向けに拡張したもの）でテストできます。本番の DB 初期化スクリプトやデータ移行コマンドにテストが無いのは、運用事故の温床です。

### 7.1 出力を検証する

```python
import click


@app.cli.command("hello")
@click.option("--name", default="World")
def hello_command(name):
    click.echo(f"Hello, {name}!")
```

```python
def test_hello_command(runner):
    # 引数なしで呼ぶ → デフォルトの "World"
    result = runner.invoke(args="hello")
    assert "World" in result.output

    # オプションを渡す → その値が出力に出る
    result = runner.invoke(args=["hello", "--name", "Flask"])
    assert "Flask" in result.output
```

`runner.invoke()` の戻り値の `result.output` に標準出力が文字列で入るので、`click.echo` した内容をそのままアサートできます。

### 7.2 `monkeypatch`：副作用の重い処理を差し替える

`init-db` のような**重い副作用を持つコマンド**は、本物の DB 初期化を走らせずに「正しい関数が呼ばれ、正しいメッセージを出すか」だけを検証したいことがあります。pytest の `monkeypatch` で内部関数を差し替え、呼び出しを記録します。

```python
def test_init_db_command(runner, monkeypatch):
    # 本物の init_db を呼んだかどうかを記録するだけのスタブ
    class Recorder:
        called = False

    def fake_init_db():
        Recorder.called = True

    # init_db を fake に差し替える（本物のDB初期化は走らない）
    monkeypatch.setattr("flaskr.db.init_db", fake_init_db)

    result = runner.invoke(args=["init-db"])

    assert "Initialized" in result.output   # ユーザー向けメッセージが出る
    assert Recorder.called                  # 実際に init_db が呼ばれた
```

ここでのポイントは、**「コマンドの責務（ユーザーに結果を伝える・正しい処理関数を起動する）」だけをテストし、その先の DB 初期化の中身は別途テストする**という関心の分離です。コマンドのテストで本物の DB を毎回作り直す必要はありません。`monkeypatch` は関数のスコープが終わると自動で元に戻すので、差し替えがテスト間に漏れる心配もありません。

---

## **8. 本番テスト戦略：API エンドポイントの正常系・異常系を固定する**

部品が揃ったので、**本番で実際に書くテスト**を組み上げます。題材は「JSON を受け取り、検証して保存し、201 を返す」典型的な API です。テストの目的は、その**契約**——「正しい入力は 201、不正な入力はエラー、未認証は弾く」——を固定することです。

```python
# tests/test_orders.py
def test_create_order_returns_201(auth_client):
    """正常系：妥当な入力で 201 と作成済みリソースが返る。"""
    response = auth_client.post(
        "/api/orders",
        json={"product_id": 10, "quantity": 3},
    )
    assert response.status_code == 201
    body = response.json
    assert body["quantity"] == 3
    assert "id" in body                       # サーバーが採番したidが返る
    assert "internal_cost" not in body        # 内部項目は出力に漏れない


def test_create_order_rejects_invalid_quantity(auth_client):
    """異常系：負の数量は 400/422 で弾かれる。"""
    response = auth_client.post(
        "/api/orders",
        json={"product_id": 10, "quantity": -1},
    )
    assert response.status_code in (400, 422)
    # フロントがどの欄にエラーを出すか判定できるよう、フィールド名を返す
    assert "quantity" in response.json["errors"]


def test_create_order_requires_auth(client):
    """認可：未認証は本処理に到達せず弾かれる。"""
    response = client.post(
        "/api/orders",
        json={"product_id": 10, "quantity": 3},
    )
    assert response.status_code in (302, 401)


def test_get_missing_order_returns_404(auth_client):
    """存在しないリソースは 404。"""
    assert auth_client.get("/api/orders/99999").status_code == 404
```

この 4 本が押さえているのは、**正常系・入力検証の異常系・認可・リソース不在**という、API の境界で起きる事故のほぼ全パターンです。`auth_client` fixture（§4.3）のおかげで、認証済み前提のテストが宣言的に書けている点にも注目してください。

> 💡 **境界の契約をテストで固定する**——これは [marshmallow × Flask × SQLAlchemy の REST API ガイド](/blog/marshmallow-flask-sqlalchemy-rest-api-production-guide) で繰り返し強調した思想と完全に一致します。あちらは「`load()` で入口を、`dump()` で出口を、スキーマの宣言で守る」という設計を、こちらは「その境界の振る舞いを `test_client` で往復検証して固定する」というテストを担います。**宣言（スキーマ）とテスト（契約の固定）の二重**で境界を守る——これが本番品質の API の条件です。エラーレスポンスの整形や 422 の返し方そのものは [エラー処理・可観測性ガイド](/blog/flask-error-handling-logging-observability-guide) で深掘りしています。

### 8.1 カバレッジと CI：テストを「常に緑」に保つ

テストは書いて終わりではなく、**CI で全コミットに対して走り続けて初めて**リグレッション防止の砦になります。

```bash
# coverage 付きで実行（pytest-cov）
pytest --cov=src/myapp --cov-report=term-missing

# CI では失敗時に分かりやすく
pytest -q --maxfail=1
```

カバレッジは**目標ではなく診断**として使います。100% を盲目的に追うと、意味の薄いテストで数字を埋める本末転倒に陥ります。重要なのは「**境界（入口・出口・認可・エラー）が網羅されているか**」であって、行カバレッジの数字そのものではありません。`--cov-report=term-missing` で「テストされていない行」を見て、それが境界やエラーパスなら埋める、という使い方が健全です。

---

## **9. テスト DB の選択：SQLite か、本物の PostgreSQL か**

最後に、現実的かつ意見の分かれる論点を正直に扱います。**テストをどの DB エンジンに対して走らせるか**です。

公式チュートリアルは、§2.2 で見たとおり**一時ファイルの SQLite**を使います。これは速く、追加のインフラが要らず、CI でも手軽です。小〜中規模アプリや、SQL が標準的な範囲に収まるアプリでは、これで十分に価値があります。

しかし、本番が **PostgreSQL** の場合、SQLite でテストすると**エンジン差**に起因する見落としが起きます。

| 観点 | SQLite（一時ファイル/インメモリ） | 本番と同じ PostgreSQL |
|---|---|---|
| 速度・手軽さ | ◎ 追加インフラ不要・高速 | △ コンテナ等の用意が要る |
| 型・制約の挙動 | △ 型が緩く、一部制約が効かない | ◎ 本番と一致 |
| 並行制御・トランザクション分離 | △ 挙動が異なる | ◎ 本番と一致 |
| Postgres 固有機能（JSONB・配列・`ON CONFLICT`・部分インデックス） | ✗ 非対応/差異あり | ◎ そのまま検証できる |

筆者の実戦での結論は、**「層で使い分ける」**です。

- **検証ロジック・スキーマ単体・ビューの分岐**など、DB エンジンに依存しない大多数のテストは **SQLite** で速く回す。
- **マイグレーション・Postgres 固有機能・トランザクション境界・一意制約の競合**に関わるテストは、CI で**本番と同じ PostgreSQL コンテナ**に対して走らせる。

> ⚠️ **「全部 SQLite で済ませる」過信の罠**：ローカルで SQLite を使った全テストが緑でも、本番 PostgreSQL では一意制約の競合や JSONB クエリで落ちる——という事故は珍しくありません。「**本番と同じエンジンに対する統合テストを CI に最低限持つ**」ことが、この差を埋める保険です。速さ（SQLite）と本番一致（Postgres）はトレードオフであり、片方に全振りせず、テストの性質で配分するのが現実解です。永続化層の挙動そのものは [SQLAlchemy 2.0 実践ガイド](/blog/sqlalchemy-2-typed-orm-production-guide) を参照してください。

CI で Postgres を使う最小例は次のようになります（GitHub Actions のサービスコンテナ）。

```yaml
# .github/workflows/test.yml（抜粋）
services:
  postgres:
    image: postgres:17
    env:
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432:5432
    options: >-
      --health-cmd pg_isready --health-interval 10s
      --health-timeout 5s --health-retries 5
```

```bash
# テスト時はこのDBへ向ける（秘密はコードに書かない）
export FLASK_SQLALCHEMY_DATABASE_URI='postgresql+psycopg://postgres:postgres@localhost:5432/test'
pytest
```

これで、ローカルは SQLite で速く、CI は本番と同じ Postgres で堅く、という二層構えが完成します。

---

## **まとめ：テスト容易性は、設計の質そのものである**

Flask のテストが容易なのは偶然ではなく、**アプリケーションファクトリ・`test_client`・pytest fixture という 3 つの設計が噛み合った結果**です。本記事の要点を再掲します。

1. **`TESTING=True`** を必ず立て、例外をテスト側に伝播させる。立て忘れるとバグが 500 に化けてデバッグが地獄になる。
2. **`app` / `client` / `runner`** の fixture 三点セットを `conftest.py` に置く。pytest は引数名で fixture を注入し、`yield` 後が後始末（一時 DB の破棄など）を担う。
3. リクエストは **`response.data`（bytes）・`response.json`・`response.text`** で本文を、`status_code` とヘッダで `Location` を検証し、必要なら `follow_redirects` + `response.history` で追う。
4. セッションは **読むとき `with client:`、仕込むとき `session_transaction()`**。後者は認証済み client の fixture 化でログインのショートカットになる。
5. **`test_request_context`** は `request` を読む関数の単体テストに（`before_request` は呼ばれない／必要なら `preprocess_request`）、**`app_context`** は DB 層のテストに。
6. CLI は **`test_cli_runner` + `monkeypatch`** で、出力と「正しい処理が呼ばれたか」を検証する。
7. **境界の契約（正常系・検証エラー・認可・404）をテストで固定**し、CI で常に緑に保つ。テスト DB は SQLite の速さと Postgres の本番一致を**層で使い分ける**。

「動く Flask アプリ」と「10 年運用できる Flask アプリ」を分けるのは、機能の数ではなく、**境界の契約がどれだけテストで固定されているか**です。そして、そのテストが素直に書けること自体が、アプリケーションファクトリで設計した配当です。本記事のテスト群は、ブラウザを介したフロント込みの動線を検証する [Playwright E2E テスト設計ガイド](/blog/playwright-e2e-testing-production-design-guide) と組み合わせることで、サーバー単体からユーザー動線までを一気通貫で守るテストピラミッドになります。各設計対象の全体地図は [Flask 本番運用ガイド](/blog/flask-production-guide) に戻って確認してください。
