testing-flask

star 1

Test Flask applications with pytest using fixtures, test client, factory data, and organized test structure.

7a336e6e By 7a336e6e schedule Updated 2/5/2026

name: Testing Flask description: Test Flask applications with pytest using fixtures, test client, factory data, and organized test structure.

Testing Flask

Goal

Set up a comprehensive testing strategy for a Flask application using pytest, with reusable fixtures, the Flask test client, factory-based test data, and a clear directory structure separating unit and integration tests.

When to Use

  • Setting up testing infrastructure for a new Flask project.
  • Adding test coverage to an existing application.
  • Writing tests for new API endpoints.
  • Reviewing or improving an existing test suite.
  • Establishing testing conventions for a team.

Instructions

Test Directory Structure

Organize tests to separate concerns:

tests/
    conftest.py           # Shared fixtures (app, client, db, auth)
    unit/
        test_user_service.py
        test_validators.py
    integration/
        test_users_api.py
        test_auth_api.py
    factories.py          # Factory Boy model factories

Core Fixtures

Define fixtures in tests/conftest.py. The app fixture creates a fresh application with test configuration. The client fixture provides the test client. The db_session fixture wraps each test in a transaction that rolls back automatically.

# tests/conftest.py

import pytest
from app import create_app
from app.extensions import db as _db


@pytest.fixture(scope="session")
def app():
    """Create the Flask application with test config."""
    app = create_app("testing")

    with app.app_context():
        _db.create_all()
        yield app
        _db.drop_all()


@pytest.fixture(scope="function")
def db_session(app):
    """Provide a transactional database session that rolls back after each test."""
    with app.app_context():
        connection = _db.engine.connect()
        transaction = connection.begin()

        options = dict(bind=connection, binds={})
        session = _db.create_scoped_session(options=options)
        _db.session = session

        yield session

        transaction.rollback()
        connection.close()
        session.remove()


@pytest.fixture(scope="function")
def client(app, db_session):
    """Flask test client with a clean database session."""
    with app.test_client() as test_client:
        yield test_client


@pytest.fixture()
def auth_headers(client):
    """Authenticate a test user and return headers with a valid token."""
    response = client.post("/api/v1/auth/login", json={
        "email": "test@example.com",
        "password": "testpassword123",
    })
    token = response.get_json()["data"]["access_token"]
    return {"Authorization": f"Bearer {token}"}

Test Data with Factory Boy

Use Factory Boy to create model instances with sensible defaults:

# tests/factories.py

import factory
from app.extensions import db
from app.models.user import User


class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session = db.session
        sqlalchemy_session_persistence = "flush"

    name = factory.Faker("name")
    email = factory.Sequence(lambda n: f"user{n}@example.com")
    role = "member"

Use factories in tests:

from tests.factories import UserFactory


def test_list_users_returns_all(client, db_session):
    UserFactory.create_batch(3)
    db_session.flush()

    response = client.get("/api/v1/users/")
    assert response.status_code == 200

    data = response.get_json()
    assert len(data["data"]) == 3

Testing Endpoints with the Test Client

The test client sends HTTP requests to the app without starting a server.

# tests/integration/test_users_api.py

import json


class TestCreateUser:
    """Tests for POST /api/v1/users/."""

    def test_create_user_success(self, client, db_session):
        payload = {
            "email": "new@example.com",
            "name": "New User",
            "role": "member",
        }

        response = client.post(
            "/api/v1/users/",
            data=json.dumps(payload),
            content_type="application/json",
        )

        assert response.status_code == 201
        data = response.get_json()
        assert data["data"]["email"] == "new@example.com"
        assert data["data"]["name"] == "New User"

    def test_create_user_missing_email(self, client, db_session):
        payload = {"name": "No Email User"}

        response = client.post(
            "/api/v1/users/",
            data=json.dumps(payload),
            content_type="application/json",
        )

        assert response.status_code == 422
        error = response.get_json()["error"]
        assert error["code"] == "VALIDATION_ERROR"
        assert "email" in error["details"]

    def test_create_user_duplicate_email(self, client, db_session):
        UserFactory(email="dupe@example.com")
        db_session.flush()

        payload = {
            "email": "dupe@example.com",
            "name": "Duplicate",
        }

        response = client.post(
            "/api/v1/users/",
            data=json.dumps(payload),
            content_type="application/json",
        )

        assert response.status_code == 409

Unit Testing Services

Test business logic independently of HTTP:

# tests/unit/test_user_service.py

from app.services.user_service import create_user
from app.exceptions import ConflictError
import pytest


def test_create_user_with_valid_data(db_session):
    user = create_user({"email": "valid@example.com", "name": "Valid"})
    assert user["email"] == "valid@example.com"


def test_create_user_duplicate_raises_conflict(db_session):
    create_user({"email": "first@example.com", "name": "First"})
    with pytest.raises(ConflictError):
        create_user({"email": "first@example.com", "name": "Duplicate"})

Running Tests

# Run all tests
pytest tests/

# Run with coverage
pytest tests/ --cov=app --cov-report=term-missing

# Run only unit tests
pytest tests/unit/

# Run a specific test
pytest tests/integration/test_users_api.py::TestCreateUser::test_create_user_success -v

Constraints

✅ Do

  • Test both the happy path and error cases for every endpoint.
  • Use fixtures for app creation, database sessions, and authentication.
  • Isolate each test with a transaction rollback so tests do not affect each other.
  • Use Factory Boy or similar for creating test data with sensible defaults.
  • Separate unit tests (service logic) from integration tests (HTTP endpoints).
  • Run tests with coverage reporting to track gaps.

❌ Don't

  • Do not test implementation details such as private methods or internal data structures.
  • Do not share mutable state between tests; each test gets a fresh database session.
  • Do not skip testing error cases (4xx responses, validation failures, edge cases).
  • Do not make tests depend on execution order.
  • Do not hardcode IDs or timestamps that may change between runs.
  • Do not mock the database in integration tests; use a real test database with rollback.

Output Format

Generate tests/conftest.py, tests/factories.py, and at least one test file. Include the pytest command to run the suite.

Dependencies

Install via CLI
npx skills add https://github.com/7a336e6e/skills --skill testing-flask
Repository Details
star Stars 1
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator