Skip to content

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

DB de test

  • Conteneur Postgres dédié, port 5433 (différent du dev en 5432).
  • DB better_search_test, user/password better_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 :

python
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 :

python
return (datetime.now() + timedelta(days=30)) - _TEST_DATA_REFERENCE_DATE

test_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 :

NomPaysCatégoriesMoisbookingStatediscount_club
Coupe du monde KitzbühelATautocar-4, skijan0non
Coupe du monde WengenCHautocar-4, skijan2non
Lac MajeurITbon-planmars0non
Réveillon en BourgogneFRreveillonsdéc2OUI
Réveillon en ArdècheFRreveillonsdéc3 (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 avec register_vector et yield ; close à la fin.
  • clean_db : DELETE FROM travels avant et après le test.
  • populated_db : clean_db + insère sample_travels avec 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 :

python
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

ClasseCouvre
TestSearchNoFilterssearch vide retourne tout, _relevance_score=1.0
TestSearchWithQuerysearch 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
TestPastTravelsHiddenUn voyage avec uniquement des dates passées disparaît
TestSearchBySeaside?seaside=true/false/absent
TestSearchCombinedFiltersCombinaisons multiples
TestSearchWithQueryAndFilterssearch + filtres mélangés

tests/test_reindex.py

ClasseCouvre
TestReindexingInsert, update existant, delete stale
TestSeasideReindexDual-source (main + /seaside), tagging is_seaside, échec /seaside
TestScheduledReindexSmoke test du module schedule_runner

tests/test_static.py

ClasseCouvre
TestAssetLinksJsonGET /.well-known/assetlinks.json (200, JSON, structure attendue)
TestWellKnownDirectory404 sur fichier inexistant, pas de listing

Lancer la suite

sh
# 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/ -x

Couverture

Configurée dans pyproject.toml :

toml
[tool.coverage.run]
source = ["src"]

[tool.coverage.report]
show_missing = true
fail_under = 70

70 % minimum. La CI échoue sous ce seuil sur master et release/*.

CI

Le job test de .gitlab-ci.yml :

  1. Image python:3.13-slim.
  2. Service sidecar pgvector/pgvector:pg17 aliasé postgres.
  3. Installe postgresql-client, curl, puis uv.
  4. Boucle pg_isready (max 30 s) pour attendre Postgres.
  5. uv sync --frozen --group test.
  6. uv run yoyo apply ... --batch.
  7. uv run pytest ... --cov-fail-under=70.
  8. 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

python
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 :

python
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

python
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 :

python
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érer function-scoped si le test modifie l'état.
  • embedding random qui matche trop bien : avec DEFAULT_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_api non utilisé : un test de search avec ?search= non vide a besoin de la fixture, sinon infomaniak.requests.post part vers le vrai endpoint et échoue.

Contributors

No contributors

Changelog

No recent changes