fix(tests): isolate SQLite test databases
Co-authored-by: OpenAI/GPT-5.6-Terra <vibecoder@antoineve.me>
This commit is contained in:
@@ -15,6 +15,7 @@ Architecture et composants clés :
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import tomllib
|
import tomllib
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
@@ -36,6 +37,10 @@ def _migrate_db(app):
|
|||||||
"""
|
"""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
if db.engine.url.database in (None, ":memory:"):
|
||||||
|
return # Une base mémoire est initialisée par create_all().
|
||||||
|
|
||||||
db_path = os.path.join(app.instance_path, "worklog.db")
|
db_path = os.path.join(app.instance_path, "worklog.db")
|
||||||
if not os.path.exists(db_path):
|
if not os.path.exists(db_path):
|
||||||
return # Nouvelle DB, create_all() s'en charge
|
return # Nouvelle DB, create_all() s'en charge
|
||||||
@@ -120,7 +125,12 @@ def _date_fr(d):
|
|||||||
return f"{jour} {d.day} {mois} {d.year}"
|
return f"{jour} {d.day} {mois} {d.year}"
|
||||||
|
|
||||||
|
|
||||||
def create_app(config_path=None):
|
def create_app(
|
||||||
|
config_path: str | None = None,
|
||||||
|
*,
|
||||||
|
database_uri: str | None = None,
|
||||||
|
engine_options: Mapping[str, object] | None = None,
|
||||||
|
) -> Flask:
|
||||||
"""Factory de création et de configuration de l'application Flask.
|
"""Factory de création et de configuration de l'application Flask.
|
||||||
|
|
||||||
Cette fonction réalise les étapes suivantes :
|
Cette fonction réalise les étapes suivantes :
|
||||||
@@ -136,6 +146,11 @@ def create_app(config_path=None):
|
|||||||
Paramètres:
|
Paramètres:
|
||||||
config_path (str | None): Chemin optionnel vers le fichier de configuration TOML.
|
config_path (str | None): Chemin optionnel vers le fichier de configuration TOML.
|
||||||
Par défaut, cherche `config.toml` à la racine du projet.
|
Par défaut, cherche `config.toml` à la racine du projet.
|
||||||
|
database_uri (str | None): URI SQLAlchemy à utiliser à la place de la base SQLite
|
||||||
|
de l'instance. Cette option est appliquée avant l'initialisation
|
||||||
|
de Flask-SQLAlchemy.
|
||||||
|
engine_options (Mapping[str, object] | None): Options SQLAlchemy appliquées avant
|
||||||
|
l'initialisation de Flask-SQLAlchemy.
|
||||||
|
|
||||||
Retourne:
|
Retourne:
|
||||||
Flask: L'instance de l'application Flask configurée et prête à l'emploi.
|
Flask: L'instance de l'application Flask configurée et prête à l'emploi.
|
||||||
@@ -144,9 +159,11 @@ def create_app(config_path=None):
|
|||||||
|
|
||||||
os.makedirs(app.instance_path, exist_ok=True)
|
os.makedirs(app.instance_path, exist_ok=True)
|
||||||
|
|
||||||
app.config["SQLALCHEMY_DATABASE_URI"] = (
|
if database_uri is None:
|
||||||
f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}"
|
database_uri = f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}"
|
||||||
)
|
app.config["SQLALCHEMY_DATABASE_URI"] = database_uri
|
||||||
|
if engine_options is not None:
|
||||||
|
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = engine_options
|
||||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||||
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-change-in-prod")
|
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-change-in-prod")
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import pytest
|
|||||||
|
|
||||||
from app import create_app
|
from app import create_app
|
||||||
from app import db as _db
|
from app import db as _db
|
||||||
|
from tests.in_memory_db import IN_MEMORY_DATABASE_URI, in_memory_engine_options
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -72,9 +73,12 @@ forfait = 0
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
application = create_app(config_path=str(config_path))
|
application = create_app(
|
||||||
|
config_path=str(config_path),
|
||||||
|
database_uri=IN_MEMORY_DATABASE_URI,
|
||||||
|
engine_options=in_memory_engine_options(),
|
||||||
|
)
|
||||||
application.config["TESTING"] = True
|
application.config["TESTING"] = True
|
||||||
application.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
|
||||||
|
|
||||||
with application.app_context():
|
with application.app_context():
|
||||||
_db.create_all()
|
_db.create_all()
|
||||||
|
|||||||
20
tests/in_memory_db.py
Normal file
20
tests/in_memory_db.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""Configuration SQLite mémoire partagée entre fixtures et helpers de tests.
|
||||||
|
|
||||||
|
Ce module est volontairement indépendant de ``conftest`` afin d'être
|
||||||
|
importable de manière portable, y compris sous ``pytest --import-mode=importlib``
|
||||||
|
(où ``conftest`` n'est pas importable comme module ordinaire). Il centralise la
|
||||||
|
configuration mémoire partagée entre la fixture ``app`` et les helpers de tests
|
||||||
|
qui appellent directement ``create_app(...)``, évitant qu'une factory de test
|
||||||
|
initialise accidentellement ``instance/worklog.db``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
IN_MEMORY_DATABASE_URI = "sqlite:///:memory:"
|
||||||
|
|
||||||
|
|
||||||
|
def in_memory_engine_options() -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"poolclass": StaticPool,
|
||||||
|
"connect_args": {"check_same_thread": False},
|
||||||
|
}
|
||||||
11
tests/test_app_factory.py
Normal file
11
tests/test_app_factory.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_fixture_uses_one_in_memory_database_connection(app):
|
||||||
|
with app.app_context():
|
||||||
|
assert str(db.engine.url) == "sqlite:///:memory:"
|
||||||
|
assert isinstance(db.engine.pool, StaticPool)
|
||||||
|
assert sa.inspect(db.engine).has_table("work_entries")
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
from tests.in_memory_db import IN_MEMORY_DATABASE_URI, in_memory_engine_options
|
||||||
|
|
||||||
_MINIMAL_CONFIG = """
|
_MINIMAL_CONFIG = """
|
||||||
[vehicles.citadine]
|
[vehicles.citadine]
|
||||||
@@ -26,7 +27,11 @@ default_motor_vehicle_id = "{vehicle_id}"
|
|||||||
def _create_app_with_home_assistant_config(tmp_path, **values):
|
def _create_app_with_home_assistant_config(tmp_path, **values):
|
||||||
config_path = tmp_path / "config.toml"
|
config_path = tmp_path / "config.toml"
|
||||||
config_path.write_text(_MINIMAL_CONFIG.format(**values), encoding="utf-8")
|
config_path.write_text(_MINIMAL_CONFIG.format(**values), encoding="utf-8")
|
||||||
return create_app(config_path=str(config_path))
|
return create_app(
|
||||||
|
config_path=str(config_path),
|
||||||
|
database_uri=IN_MEMORY_DATABASE_URI,
|
||||||
|
engine_options=in_memory_engine_options(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_vehicles_returns_configured_vehicles(app):
|
def test_get_vehicles_returns_configured_vehicles(app):
|
||||||
@@ -164,4 +169,8 @@ timezone = "Europe/Paris"
|
|||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="clé.*manquante"):
|
with pytest.raises(ValueError, match="clé.*manquante"):
|
||||||
create_app(config_path=str(config_path))
|
create_app(
|
||||||
|
config_path=str(config_path),
|
||||||
|
database_uri=IN_MEMORY_DATABASE_URI,
|
||||||
|
engine_options=in_memory_engine_options(),
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user