【Flask】pytestでFlaskアプリの単体テストを書く

Flask

こんにちは、かつコーチです。

ルーティング・データベース・ログイン機能と実装が進んでくると、「コードを直しても、他の部分が壊れていないか」を毎回手動で確認するのが大変になってきます。

この記事では、Pythonの定番テストツールpytestを使って、Flaskアプリの単体テストを書く方法を解説します。

なぜFlaskアプリにテストが必要なのか

手動確認の限界

機能が少ないうちは、ブラウザで動作確認する「手動テスト」で十分かもしれません。

しかし、ログイン機能・CRUD機能・API機能と増えていくと、1つの修正が既存の機能にどう影響するかを毎回ブラウザですべて確認するのは現実的ではなくなります。

単体テスト(1つの機能・関数が正しく動くかを自動で検証するコード)を書いておくことで、コマンド1つで全機能の動作確認を一瞬で終えられるようになります。

pytestとFlaskの相性

pytestはPython全般で使われる汎用テストツールですが、Flaskにはtest_clientというテスト専用の仕組みが用意されており、実際にサーバーを起動しなくてもリクエストのシミュレーションができます。

pip install pytest

基本の書き方・実装手順

手順1: テスト用のアプリ構成に整理する

テストを書きやすくするため、アプリの生成をファクトリ関数として切り出しておくのが定石です。

# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app(testing=False):
    app = Flask(__name__)
    if testing:
        app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
        app.config["TESTING"] = True
    else:
        app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"

    db.init_app(app)

    @app.route("/health")
    def health():
        return {"status": "ok"}

    return app

このアプリケーションファクトリパターン(設定を切り替えられる形でアプリを生成する書き方)により、テスト時には本番用データベースを使わず、メモリ上だけで完結するデータベースに切り替えられます。

手順2: pytestのfixtureを用意する

tests/conftest.pyというファイルに、テストの共通準備を書きます。

# tests/conftest.py
import pytest
from app import create_app, db

@pytest.fixture
def app():
    app = create_app(testing=True)
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

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

fixture(テストの前後に必要な準備・後片付けを共通化する仕組み)を使うことで、各テスト関数の冒頭に同じセットアップコードを書かずに済みます。

clientは、実際にHTTPリクエストを送るのと同じようにルートをテストできる、Flask標準のテストクライアントです。

手順3: 実際のテストコードを書く

# tests/test_health.py
def test_health_check(client):
    response = client.get("/health")
    assert response.status_code == 200
    assert response.get_json() == {"status": "ok"}

assert(この条件が真であることを確認する、という意味のPython構文)を使って、期待する結果と実際の結果を比較します。

テストを実行するには、ターミナルで次のコマンドを実行します。

pytest

.(ドット)が表示されればテスト成功、Fが表示されれば失敗を意味します。

手順4: CRUD機能のテストを書く

前回・前々回で扱ったCRUD機能も、同じ要領でテストできます。

def test_create_task(client):
    response = client.post("/tasks", json={"title": "牛乳を買う"})
    assert response.status_code == 201
    data = response.get_json()
    assert data["title"] == "牛乳を買う"

def test_get_tasks_returns_created_task(client):
    client.post("/tasks", json={"title": "掃除をする"})
    response = client.get("/tasks")
    titles = [task["title"] for task in response.get_json()]
    assert "掃除をする" in titles

client.postclient.getで、実際のブラウザからのリクエストと同じ形式のテストが書けます。

つまずきやすい設定・注意点

テスト用データベースを必ず分離する

テスト実行時に本番用のデータベース(app.dbなど)をそのまま使ってしまうと、テストで作成したダミーデータが本番データに混ざってしまいます。

create_app(testing=True)のように、テスト用の設定では必ずsqlite:///:memory:(メモリ上だけに存在し、プロセス終了と同時に消えるデータベース)を使うようにしましょう。

fixtureのスコープを意識する

@pytest.fixtureはデフォルトで「各テスト関数ごと」に実行されます。

テストごとにdb.create_all()db.drop_all()が繰り返されるため、あるテストで作ったデータが別のテストに影響を与えることを防げます。

よくあるつまずきポイント・エラー対処

sqlalchemy.exc.OperationalError: no such table

❌ Before:conftest.pyappフィクスチャでdb.create_all()を呼び忘れ、テスト実行時にsqlalchemy.exc.OperationalError: no such table: taskというエラーが出る

✅ After:appフィクスチャの中で必ずwith app.app_context(): db.create_all()を実行してからyieldする

私が最初にこのエラーに遭遇したときは、「アプリ自体は正しく動くのに、なぜテストだけ失敗するのか」と混乱しました。

原因は、テスト用のメモリ上データベースは毎回まっさらな状態から始まるため、テストの実行前に明示的にテーブルを作成する処理を書く必要がある、という点でした。

本番運用ではマイグレーション済みのファイルがすでに存在するため気づきにくい、テスト特有のつまずきポイントです。

テストの実行順序に依存した失敗

❌ Before:「1件目のタスクのIDは必ず1になる」という前提でテストを書いてしまい、他のテストの実行順序によってIDがずれ、テストが不安定に失敗する(Flaky Testと呼ばれる状態)

✅ After:IDのような可変の値に依存せず、response.get_json()["title"] == "牛乳を買う"のように、そのテストで確認したい値だけを検証する

テストが「たまに失敗する」状態は原因調査に時間がかかるため、最初から実行順序に依存しないテストの書き方を意識しておくと後々楽になります。

応用・一歩先の使い方

ログイン機能を含むテスト

前回解説したFlask-Loginの機能もテストできます。

def test_login_required_redirects(client):
    response = client.get("/dashboard")
    assert response.status_code == 302  # ログインページへリダイレクト

def test_login_success(client):
    # 事前にユーザー登録処理を実行しておく想定
    response = client.post("/login", data={"email": "test@example.com", "password": "password123"})
    assert response.status_code == 302

follow_redirects=Trueclient.postに渡すことで、リダイレクト先のページまで含めて検証することも可能です。

カバレッジを計測する

pytest-covというライブラリを使うと、コード全体のうち何%がテストで実行されたかを可視化できます。

pip install pytest-cov
pytest --cov=app

テストが不足している箇所を客観的に把握できるため、テストの網羅性を高めたい中級者以上の方におすすめです。

まとめ

この記事のポイント

  • pytestとFlaskのtest_clientを組み合わせることで、サーバー起動なしにルートのテストができる
  • アプリケーションファクトリパターンで、本番用とテスト用の設定を切り替える
  • テスト用データベースはsqlite:///:memory:で本番と分離する
  • db.create_all()の呼び忘れがテスト特有のエラー原因になりやすい
  • IDなど可変の値に依存したテストはFlaky Testの原因になる

次に読むべき記事

テストで品質を担保できるようになったら、いよいよアプリを世の中に公開する番です。

「FlaskアプリをRenderにデプロイする方法」で、公開までの手順を解説します。


タグ: Flask, 中級者向け, テスト

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