Module — Tests
Suite pytest qui couvre la recherche, le reindex, et le serving de fichiers statiques. ~860 lignes de tests pour ~600 lignes de code applicatif.
Inventaire
tests/
├── __init__.py
├── conftest.py — fixtures pytest partagées
├── test_db.json — fixture : 5 records voyage de référence
├── test_embeddings.json — fixture : embeddings 3584D des 5 voyages
├── test_query_embeddings.json — fixture : embeddings des queries de test
├── test_reindex.py — pipeline d'ingestion
├── test_search.py — recherche + filtres
└── test_static.py — endpoint /.well-known/assetlinks.jsonDB de test
- Conteneur Postgres dédié, port 5433 (différent du dev en 5432).
- DB
better_search_test, user/passwordbetter_search/better_search_pwd. - Lancée via
docker-compose-test.yml. - Migrations appliquées avant le run :
uv run yoyo apply --database "$TEST_DATABASE_URL" ./migrations --batch.
L'URL est lue depuis TEST_DATABASE_URL si défini (CI), sinon conftest.py la hardcode :
os.environ['DATABASE_URL'] = os.environ.get(
'TEST_DATABASE_URL',
'postgresql://better_search:better_search_pwd@localhost:5433/better_search_test'
)Fixtures clés (conftest.py)
date_offset (session-scoped)
Calcule un timedelta qui décale les dates de la fixture (référence interne 2025-12-29) vers le futur. Garantit que les départs tombent toujours dans le futur, peu importe quand on lance les tests :
return (datetime.now() + timedelta(days=30)) - _TEST_DATA_REFERENCE_DATEtest_db_response (session-scoped)
Charge tests/test_db.json, applique date_offset à toutes les occurrences[*].start/end, et retourne le dict.
sample_travels
Liste des 5 records après décalage. Contenu :
| Nom | Pays | Catégories | Mois | bookingState | discount_club |
|---|---|---|---|---|---|
| Coupe du monde Kitzbühel | AT | autocar-4, ski | jan | 0 | non |
| Coupe du monde Wengen | CH | autocar-4, ski | jan | 2 | non |
| Lac Majeur | IT | bon-plan | mars | 0 | non |
| Réveillon en Bourgogne | FR | reveillons | déc | 2 | OUI |
| Réveillon en Ardèche | FR | reveillons | déc | 3 (INVALID) | OUI |
test_embeddings / test_query_embeddings
Dictionnaires id/text → embedding 3584D. Pré-calculés contre l'API Infomaniak puis snapshottés en JSON.
mock_infomaniak_api
Patche src.infomaniak.requests.post. Retourne :
- Embedding stockée si le texte exact est dans
test_query_embeddings. - Sinon embedding pseudo-aléatoire mais déterministe (seed =
hash(text)).
mock_slow_search_api
Patche src.reindex.requests.get. Sur l'URL principale, retourne test_db_response à la page 1, puis [] aux pages suivantes. Sur l'URL /seaside, retourne toujours [] (préserve les comptes pré-3.2.0).
db_connection / clean_db / populated_db
db_connection: ouvre une connexion psycopg avecregister_vectoret yield ; close à la fin.clean_db: DELETE FROM travels avant et après le test.populated_db:clean_db+ insèresample_travelsavec leurs embeddings de référence.
search_db
Crée une instance BuchardDatabase(pool=..., auto_reindex=False) contre la DB peuplée. Yield, puis ferme le pool.
Variables d'env imposées par les tests
conftest.py les set avant tout import :
os.environ['SLOW_SEARCH_URL'] = 'https://test.example.com/api/travels'
os.environ['INFOMANIAK_PID'] = 'test_pid'
os.environ['INFOMANIAK_TOKEN'] = 'test_token'
os.environ['DEFAULT_RRF_VECTOR_WEIGHT'] = '0.5'
os.environ['DEFAULT_SEMANTIC_FLOOR'] = '0.0' # tout garder en test
os.environ['DEFAULT_TSVECTOR_FLOOR'] = '0.0'Les planchers sont à 0 pour ne pas filtrer involontairement les fixtures (les embeddings random ont une similarité ~0.4-0.6 entre eux).
Classes de tests
tests/test_search.py
| Classe | Couvre |
|---|---|
TestSearchNoFilters | search vide retourne tout, _relevance_score=1.0 |
TestSearchWithQuery | search non vide retourne des résultats avec scores variables |
TestSearchByDestination | ?destination=FR filtre correctement |
TestSearchByCategory | ?category=ski, ?category=ski,bon-plan (OR) |
TestSearchByDates | ?dates= mono-range et multi-range |
TestSearchByDiscountClub | ?discountclub=true/false |
TestSearchByValidity | ?hide_invalid=true |
TestPastTravelsHidden | Un voyage avec uniquement des dates passées disparaît |
TestSearchBySeaside | ?seaside=true/false/absent |
TestSearchCombinedFilters | Combinaisons multiples |
TestSearchWithQueryAndFilters | search + filtres mélangés |
tests/test_reindex.py
| Classe | Couvre |
|---|---|
TestReindexing | Insert, update existant, delete stale |
TestSeasideReindex | Dual-source (main + /seaside), tagging is_seaside, échec /seaside |
TestScheduledReindex | Smoke test du module schedule_runner |
tests/test_static.py
| Classe | Couvre |
|---|---|
TestAssetLinksJson | GET /.well-known/assetlinks.json (200, JSON, structure attendue) |
TestWellKnownDirectory | 404 sur fichier inexistant, pas de listing |
Lancer la suite
# Avec coverage
uv run pytest tests/ -v --cov=src --cov-report=term-missing
# Couverture minimum requise (cf. pyproject.toml et CI)
uv run pytest tests/ -v --cov=src --cov-fail-under=70
# Un seul test
uv run pytest tests/test_search.py::TestSearchByDestination::test_filter_by_destination -v
# Stop au premier échec
uv run pytest tests/ -xCouverture
Configurée dans pyproject.toml :
[tool.coverage.run]
source = ["src"]
[tool.coverage.report]
show_missing = true
fail_under = 7070 % minimum. La CI échoue sous ce seuil sur master et release/*.
CI
Le job test de .gitlab-ci.yml :
- Image
python:3.13-slim. - Service sidecar
pgvector/pgvector:pg17aliasépostgres. - Installe
postgresql-client,curl, puisuv. - Boucle
pg_isready(max 30 s) pour attendre Postgres. uv sync --frozen --group test.uv run yoyo apply ... --batch.uv run pytest ... --cov-fail-under=70.- Coverage exposée via la regex
total.*?(\d+(?:\.\d+)?\%).
Règles de blocage : strict sur master et release/*, allow_failure: true ailleurs.
Patterns de tests à suivre
Toujours auto_reindex=False dans les fixtures
db = BuchardDatabase(pool=pool, auto_reindex=False)Sinon importer src.db déclenche un reindex au load du module.
Mocker les deux sources amont
Si vous mockez requests.get, n'oubliez pas la branche /seaside :
is_seaside_url = '/seaside' in url.split('?')[0]
if is_seaside_url:
response.json.return_value = { ..., 'records': [], 'totalFilteredRecords': 0 }Sinon le reindex plante en récupérant la 2e source, même si la 1ère a OK.
Pour les dates : utiliser shifted_date
def test_filter_by_date(self, search_db, shifted_date):
jan_start = shifted_date("2026-01-01")
jan_end = shifted_date("2026-01-31")
results, _ = search_db.search(dates=f"{jan_start}_{jan_end}")Sinon dans 6 mois les tests deviennent flaky parce que les dates 2026 sont passées.
Pour appeler le HTTP : patcher le singleton db
Cf. TestSearchBySeaside::test_seaside_filter_via_http : le serveur utilise db = BuchardDatabase() au top-level de src/server.py. Pour un test FastAPI client, monkeypatch _db.search :
from src.db import db as _db
_db.search = seaside_search_db.search
client = TestClient(app)
resp = client.get('/travels?seaside=true')Pièges fréquents
- Test qui passe en isolation, plante en suite : généralement un fixture
session-scoped(ex:date_offset) qui n'est pas réinitialisé. Préférerfunction-scoped si le test modifie l'état. embedding randomqui matche trop bien : avecDEFAULT_SEMANTIC_FLOOR=0, un random vector peut quand même ressortir. Pour les tests de relevance, utiliser les embeddings réels du fixture (get_travel_embedding(travel_id)).mock_infomaniak_apinon utilisé : un test de search avec?search=non vide a besoin de la fixture, sinoninfomaniak.requests.postpart vers le vrai endpoint et échoue.

