Testing Flask Applications
Introduction
Automated tests catch regressions before deploy. Flask provides a test client that simulates HTTP requests without starting a network server. pytest fixtures wrap create_app('testing'), in-memory SQLite, and logged-in sessions. This chapter tests routes, JSON APIs, and database behavior safely.
Prerequisites
- Blueprints and Application Factory
- Flask-SQLAlchemy and Models
pip install pytest
Install pytest
pip install pytestrequirements-dev.txt (optional):
pytest==8.3.3
pytest-cov==5.0.0Run:
pytest
pytest -v tests/test_routes.pyTesting Config
app/config.py:
class TestingConfig(Config):
TESTING = True
DEBUG = True
WTF_CSRF_ENABLED = False
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
SECRET_KEY = "test-secret"Code explanation:
TESTING=Trueenables test behaviors in some extensions- In-memory SQLite is fast and isolated per test run
App and Client Fixtures
tests/conftest.py:
Code explanation:
app.app_context()required fordboperations outside requestscreate_all/drop_allreset schema between tests
Test a Route
tests/test_main.py:
def test_index(client):
response = client.get("/")
assert response.status_code == 200
assert b"Home" in response.data
def test_404(client):
response = client.get("/does-not-exist")
assert response.status_code == 404Follow redirects:
response = client.get("/old-url", follow_redirects=True)
assert response.status_code == 200Test JSON API
Test Forms and POST
def test_contact_form(client):
response = client.post(
"/contact",
data={"name": "Alice", "email": "a@example.com", "message": "Hello there!"},
follow_redirects=True,
)
assert response.status_code == 200
assert b"Thanks" in response.dataWith CSRF enabled in tests, either disable WTF_CSRF_ENABLED or extract token from GET form HTML.
Database Tests
from app.models import User
from app.extensions import db
def test_user_create(app):
with app.app_context():
user = User(username="bob", email="bob@example.com")
user.set_password("secret")
db.session.add(user)
db.session.commit()
found = User.query.filter_by(username="bob").first()
assert found is not None
assert found.check_password("secret")Authenticated Client Fixture
Flask-Login:
Code explanation:
session_transaction()sets session before request—simulates logged-in user_user_idkey matches Flask-Login session format
JWT tests:
def test_protected_api(client, app):
login = client.post("/api/v1/login", json={"username": "tester", "password": "pass"})
token = login.get_json()["access_token"]
response = client.get(
"/api/v1/posts",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200CLI Commands
def test_seed_command(runner):
result = runner.invoke(args=["seed-db"])
assert result.exit_code == 0
assert "Seeded" in result.outputRegister with @app.cli.command("seed-db").
Coverage (Optional)
pytest --cov=app --cov-report=term-missingFocus tests on business logic and auth—not every template pixel.
Tip
Test Behavior, Not Implementation
Assert status codes and key response content; avoid coupling to internal function names.
CI Link
Run pytest in GitHub Actions—see Git and CI/CD.
FAQ
RuntimeError: working outside application context?
Wrap DB code in with app.app_context(): or use fixtures that provide app.
Database not empty between tests?
Use function-scoped app fixture with drop_all or transactions per test.
JSON decode error?
Check response.is_json and status code before get_json().
Tests pass locally, fail in CI?
CI may lack env vars—set test defaults in TestingConfig.
Parallel pytest?
Shared SQLite file conflicts—use sqlite:///:memory: per worker or pytest-xdist with isolated DB.
Mock external APIs?
Use unittest.mock.patch or pytest-mock for HTTP clients and mail.