はじめに:なぜテストが重要なのか?
このガイドへようこそ!ここでは、Flaskアプリケーションの品質を保証するための自動テストの世界に飛び込みます。特に、強力で直感的なテストフレームワークである`pytest`に焦点を当てます。自動テストを導入することで、品質の向上、安全なリファクタリング、開発速度の向上、そして「生きたドキュメント」の作成といった、数多くのメリットが得られます。
1. 導入と最初のテスト
まずはテスト環境を整え、最初のテストを実行してみましょう。`pytest`は非常にシンプルに始められます。
1.1. 必要なパッケージのインストール
pip install flask pytest pytest-flask
1.2. アプリケーションファクトリによるFlaskアプリ作成
テスト対象となるアプリケーションを作成します。ここで重要なのが**「アプリケーションファクトリ」**というパターンです。これは、アプリケーションのインスタンスを生成する関数を定義する、Flaskのベストプラクティスです。
`app = Flask(__name__)` とグローバルに書くのと違い、`create_app` 関数を使うことで、テストごとに異なる設定(例:テスト用データベース)を適用したインスタンスを動的に作成できます。これにより、テストの独立性が保たれ、信頼性の高いテストが書けます。
# my_app/__init__.py
from flask import Flask, jsonify
def create_app():
app = Flask(__name__)
@app.route('/')
def index():
return "Hello, World!"
return app
1.3. `pytest`のための設定 (`conftest.py`)
`tests/conftest.py`という特別なファイルに、テスト全体で共有する設定(フィクスチャ)を定義します。
# tests/conftest.py
import pytest
from my_app import create_app
@pytest.fixture
def app():
app = create_app()
app.config.update({"TESTING": True})
yield app
@pytest.fixture
def client(app):
return app.test_client()
`create_app()` が返す `app` はFlaskクラスのインスタンスで、便利な属性やメソッドを最初から持っています。
- `app.config`: アプリケーションの設定値を保持する辞書のような属性です。
- `app.test_client()`: サーバーを起動せずにリクエストを送信できる仮想クライアントを作成するメソッドです。
`test_`で始まる関数がテストとして認識されます。
# tests/test_simple.py
def test_index_route(client):
response = client.get('/')
assert response.status_code == 200
assert b"Hello, World!" in response.data
2. 単体テストの実践
単体テストは、関数やクラスといった個々の部品が独立して正しく動作することを確認します。
# my_app/models.py
class User:
def __init__(self, name, email, is_active=True):
self.name = name
self.email = email
self.is_active = is_active
@property
def is_admin(self):
return self.email.endswith('@admin.example.com')
# tests/test_models.py
from my_app.models import User
def test_user_is_admin():
admin_user = User("Admin", "admin@admin.example.com")
normal_user = User("Normal", "normal@example.com")
assert admin_user.is_admin is True
assert normal_user.is_admin is False
3. 結合テストの実践
結合テストは、ルートとデータベースなど、複数のコンポーネントが連携して動作することを確認します。
# my_app/__init__.py
from flask import Flask, jsonify, request
def create_app():
app = Flask(__name__)
users_db = {
1: {"name": "Alice"}, 2: {"name": "Bob"}
}
app.config['USERS_DB'] = users_db
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
user = app.config['USERS_DB'].get(user_id)
if user: return jsonify(user)
return jsonify({"error": "Not found"}), 404
return app
# tests/test_users_api.py
import json
def test_get_user_success(client):
response = client.get('/api/users/1')
assert response.status_code == 200
data = json.loads(response.data)
assert data['name'] == 'Alice'
def test_get_user_not_found(client):
response = client.get('/api/users/999')
assert response.status_code == 404
4. 応用的なテクニック
より効率的で強力なテストを書くためのテクニックです。
4.1. パラメータ化 (`parametrize`)
同じテストロジックを異なる入力値で何度も実行したい場合に、テストコードの重複を避けられます。
# tests/test_advanced.py
import pytest
@pytest.mark.parametrize("user_id, expected_status", [
(1, 200), (2, 200), (999, 404), ("abc", 404)
])
def test_get_user_with_various_ids(client, user_id, expected_status):
response = client.get(f'/api/users/{user_id}')
assert response.status_code == expected_status
4.2. モック (`monkeypatch`)
外部APIなど、テストに不向きな処理を偽のオブジェクトに置き換えます。
# my_app/services.py
def get_weather_from_api(city):
# 本来はここで外部APIを呼び出す...
return {"temp": 25, "condition": "sunny"}
# tests/test_weather.py
from my_app import services
def test_get_weather_with_mock(client, monkeypatch):
# get_weather_from_apiを、常に固定値を返す関数に置き換える
monkeypatch.setattr(services, 'get_weather_from_api',
lambda city: {"temp": 15, "condition": "rainy"})
# ... ここで /api/weather/london にリクエストを送るテストを書く ...
5. プロフェッショナルなテスト環境の構築
プロジェクト全体のテスト品質と信頼性を保証するための、より高度なツールとワークフローです。
5.1. `tox`による複数環境でのテスト
`tox`は、異なるPythonバージョンやライブラリ依存関係に対する互換性を、手軽かつ確実に検証するツールです。
[tox]
envlist = py39, py310, py311
isolated_build = True
[testenv]
commands = pytest {posargs}
deps =
-r requirements.txt
pytest
pytest-flask
インストール
pip install tox
実行
tox
プロジェクトルートで`tox`コマンドを実行すると、`envlist`で指定された全Python環境でテストが自動実行されます。
5.2. `factory-boy`によるテストデータの生成
`factory-boy`は、データベースモデルのテストデータを柔軟かつ効率的に生成するライブラリです。
# my_app/models.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
# tests/factories.py
import factory
from my_app.models import User, db
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = User
sqlalchemy_session = db.session
sqlalchemy_session_persistence = "commit"
id = factory.Sequence(lambda n: n)
username = factory.Sequence(lambda n: f"user{n}")
email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
# tests/test_with_factories.py
from tests.factories import UserFactory
# db_sessionはDBセッションを提供するフィクスチャと仮定
def test_user_creation_with_factory(db_session):
# ユーザーを3人作成
UserFactory.create_batch(3)
# 3人目のユーザーを検証
user3 = User.query.get(3)
assert user3.username == "user3"
assert user3.email == "user3@example.com"
5.3. E2E(End-to-End)テストの導入
E2Eテストは、実際のブラウザを自動操作し、ユーザーの視点からアプリケーション全体(フロントエンド+バックエンド)の動作を保証します。
# my_app/__init__.py
from flask import Flask, render_template
def create_app():
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', user_count=2)
return app
<!DOCTYPE html>
<html lang="ja">
<head><title>My App</title></head>
<body>
<h1 id="main-heading">Hello, World!</h1>
<p>ユーザー数: <span id="user-count">{{ user_count }}</span></p>
</body>
</html>
`pytest-playwright`と`pytest-flask`の`live_server`を使ってテストします。
# tests/test_e2e.py
import pytest
from playwright.sync_api import Page, expect
# Flaskサーバーをテスト中に起動する
@pytest.mark.usefixtures("live_server")
def test_home_page_e2e(page: Page):
# live_serverが提供するURLにアクセス
page.goto("http://localhost:5000/")
# h1タグのテキストが "Hello, World!" であることを確認
main_heading = page.locator("#main-heading")
expect(main_heading).to_have_text("Hello, World!")
# ユーザー数が "2" であることを確認
user_count_span = page.locator("#user-count")
expect(user_count_span).to_have_text("2")