Testing

Flask ships with a test client, so you can unit-test and integration-test routes and business logic without starting a real HTTP server. It pairs best with pytest.

Installing Dependencies

pip install pytest pytest-cov

Project Structure

project/
  app/
    __init__.py      # create_app factory
  tests/
    conftest.py      # shared fixtures
    test_routes.py
    test_models.py

Fixtures (conftest.py)

This is where the application-factory pattern pays off in testing — create an independently configured app instance for tests:

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

@pytest.fixture()
def app():
    app = create_app()
    app.config.update(
        TESTING=True,                                   # Disable error catching; exceptions surface in tests
        SQLALCHEMY_DATABASE_URI="sqlite:///:memory:",   # In-memory database for tests
        WTF_CSRF_ENABLED=False,                         # Disable CSRF when testing forms
    )
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

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

@pytest.fixture()
def runner(app):
    return app.test_cli_runner()    # For testing custom flask CLI commands

Testing Routes

# tests/test_routes.py
def test_index(client):
    res = client.get("/")
    assert res.status_code == 200
    assert b"Hello" in res.data

def test_create_user_api(client):
    res = client.post("/api/v1/users", json={"name": "Alice", "email": "a@example.com"})
    assert res.status_code == 201
    assert res.get_json()["name"] == "Alice"   # Responses also support get_json()

def test_404(client):
    res = client.get("/no-such-page")
    assert res.status_code == 404

client.get/post/put/delete accept json= (auto-serialized with the right Content-Type), data= (form data), headers=, query_string=, and more.

Testing Logged-In State

session_transaction() lets you read/write the session outside a request:

def test_dashboard_requires_login(client):
    res = client.get("/dashboard")
    assert res.status_code == 302               # Redirected when not logged in

def test_dashboard_logged_in(client):
    with client.session_transaction() as sess:
        sess["uid"] = 1
    res = client.get("/dashboard")
    assert res.status_code == 200

Testing Code That Needs an App Context

Code outside views (model methods, utilities) that touches current_app or the database needs a context pushed manually:

def test_model(app):
    with app.app_context():
        user = User(name="Bob")
        db.session.add(user)
        db.session.commit()
        assert user.id is not None

Running Tests and Coverage

pytest                                        # Run everything
pytest tests/test_routes.py -k create -v      # Filter by name
pytest --cov=app --cov-report=term-missing    # Coverage report with uncovered line numbers

Practical Advice

  • Keep tests independent: in-memory database plus create/drop in fixtures prevents cross-test pollution.
  • Test behavior (status codes, response content, database side effects), not internal implementation details.
  • Stub external services (email, third-party APIs) with unittest.mock or the responses library.