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

Install pytest

bash
pip install pytest

requirements-dev.txt (optional):

text
pytest==8.3.3
pytest-cov==5.0.0

Run:

bash
pytest
pytest -v tests/test_routes.py

Testing Config

app/config.py:

python
class TestingConfig(Config):
    TESTING = True
    DEBUG = True
    WTF_CSRF_ENABLED = False
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
    SECRET_KEY = "test-secret"

Code explanation:

  • TESTING=True enables 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 for db operations outside requests
  • create_all / drop_all reset schema between tests

Test a Route

tests/test_main.py:

python
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 == 404

Follow redirects:

python
response = client.get("/old-url", follow_redirects=True)
assert response.status_code == 200

Test JSON API

Test Forms and POST

python
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.data

With CSRF enabled in tests, either disable WTF_CSRF_ENABLED or extract token from GET form HTML.

Database Tests

python
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_id key matches Flask-Login session format

JWT tests:

python
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 == 200

CLI Commands

python
def test_seed_command(runner):
    result = runner.invoke(args=["seed-db"])
    assert result.exit_code == 0
    assert "Seeded" in result.output

Register with @app.cli.command("seed-db").

Coverage (Optional)

bash
pytest --cov=app --cov-report=term-missing

Focus 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.

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.