08.04 — Stratégie de test#
Six types de tests, six rôles distincts. Documentés dans
CURA_TEST_STRATEGY.md§3.
Types de test#
| Type | Statut attendu | Source d’autorité |
|---|---|---|
| Happy path (cas passant) | PASS | Parcours nominaux décrits d’après les SFD |
| Unhappy path (cas non passant) | PASS (le test passe quand l’application refuse de “laisser passer”) | Validations existantes correctes |
| Edge case / boundary (tests aux limites) | PASS ou FAIL | Selon le test, souvent accompagné d’une note ajoutée sur les SFD |
| Business logic vulnerability / gap test (tests fonctionnels pour essayer de faire “tomber” le produit et d’identifier des gaps) | FAIL intentionnel | Comportements qu’un système devrait empêcher mais que CURA n’empêche pas |
| Exploratory / observed-behaviour (exploratoire) | PASS | Documente le comportement actuel quand la spec ne le couvre pas |
| Permanent security regression fixture (non-régression) | PASS (= la sécurité tient) | Vérifie qu’un correctif de sécurité tient (correctifs qui resteront définitivement dans le patrimoine de test) |
Happy path#
Exercises the nominal flow with valid inputs and an authenticated user. Verifies that the system produces the correct output (confirmation, history entry, etc.).
valid_login.py — connexion avec identifiants valides → on s’attend à une connexion en succès.
Unhappy path#
Exercises flows where the user provides invalid input or acts outside their authorisation. The test passes when the application responds with the correct rejection.
- Connexion avec mauvais identifiants → on s’attend à un message d’erreur.
- Empty creds → on s’attend à un message d’erreur.
- Accès non autorisé à la page d’historique de consultations → on s’attend à une redirection.
- Envoi d’un formulaire de prise de RDV sans avoir précisé la date → on s’attend à ce que l’envoi soit refusé.
Edge case / boundary#
Exercises the limits of what the application is supposed to accept or reject, where the specification is unclear or the behaviour is surprising.
Synchronisé avec le document de spécifications (CURA_FRD.md) : on y ajoute une note quand un gap est trouvé.
Business logic vulnerability / gap test#
Tests intentionnellement en échec jusqu’à livraison de correctifs :
| Test | Gap | SFD |
|---|---|---|
Appointment - Server accepts empty date when client bypass applied | G-DATA-1 | §9.1 |
Appointment - Past date booking accepted | G-DATA-1 | §9.7 |
Appointments - Duplicate booking | G-DATA-2 | §9.6 |
Appointments - Overlapping appointments | G-DATA-2 | §9.6 |
Journey - History ordered most-recent date first | G-SPEC-1 | §9.8 |
Implementation rule: gap tests must fail with a clear, specific message — not a generic
TimeoutException. They useConfirmationPage.verify_not_confirmed(), which checksurl_contains("appointment.php")(fast, not DOM-dependent, not blocked by HTML5 validation popups). The assertion window is fixed at_NOT_CONFIRMED_TIMEOUT(10 s, defined inconfirmation.py) and is intentionally independent of--wait-timeout.
→ Discipline : un gap test doit échouer clairement.
_NOT_CONFIRMED_TIMEOUT est fixe (10s) et délibérément décorrélé de --wait-timeout.
Si on augmente --wait-timeout pour stabiliser un autre test, on ne veut pas risquer de faire accidentellement passer un gap test par effet de bord.
Distinguer gap test vs validation test#
Distinguishing a gap test from a validation pass:
- Gap test (intentionally FAILS): CURA should reject X but doesn’t. The test asserts rejection; the failure documents the gap.
- Validation test (PASSES): CURA does correctly reject X. The test asserts rejection; the pass confirms the validation still works.
Deux tests peuvent avoir des intentions opposées. Le nom du test, la docstring et la ligne dans IDENTIFIED_GAPS.md sont là pour les distinguer.
Exploratory / observed-behaviour#
Documents what CURA actually does when no requirement is specified. These tests pass and record the observed behaviour both in the per-test log and in the SFD.
saturation_booking.py — 5 réservations consécutives sur des dates différentes en une session, toutes acceptées. Documente l’absence de rate limit.
When to add one:
- A business-logic constraint is plausible but absent from the SFD (“can I book the same date twice in different slots?”).
- Decide before writing: is the current behaviour reasonable (→ exploratory / pass) or a gap (→ assertive / intentional fail)?
- Always pair with an SFD update.
→ Décision humaine avant l’écriture : OK ou gap ?
- Si OK : test exploratoire en succès.
- Si gap : test assertif destiné à être intentionnellement en échec.
Tests de régression persistants#
Vérifie qu’un correctif de sécurité (donc correctif critique) tient.
Si un jour le correctif casse : ce test s’allume à nouveau en rouge. Documente le maintien de la correction.
cycles/e2e.py#
E2E_CYCLE_NAME = "CURA E2E"
def create_e2e_test_cycle(drivers_pool) -> TestCycle[WebDriver]:
return TestCycle(
name=E2E_CYCLE_NAME,
smoke_tests_campaigns=[create_prerequisites_campaign(drivers_pool=drivers_pool)],
campaigns=[
create_authentication_campaign(drivers_pool=drivers_pool),
create_appointments_campaign(drivers_pool=drivers_pool),
create_profile_campaign(drivers_pool=drivers_pool),
create_user_journeys_campaign(drivers_pool=drivers_pool),
],
)Hiérarchie#
Cycle "CURA E2E"
├── Smoke (gate, fail-fast)
│ └── Campaign "Prerequisites"
│ └── Suite "Authentication baseline"
│ └── Test "valid_login" # gate
│
└── Main
├── Campaign "Authentication"
│ ├── Suite "Session management"
│ └── Suite "Failure modes"
│ ├── failed_logins.py # data-driven : invalid / empty / wrong-case
│ ├── unauthenticated_appointment_access.py
│ ├── unauthenticated_history_access.py
│ ├── unauthenticated_profile_access.py
│ ├── logout.py
│ ├── post_logout_access.py
│ ├── post_logout_bfcache_exposure.py # B-BROWSER-1
│ ├── post_logout_frenetic_navigation.py
│ ├── post_logout_server_invalidation.py
│ └── rapid_logout_relogin.py
├── Campaign "Appointments"
│ ├── Suite "Booking"
│ │ ├── book_appointments.py # data-driven : Hongkong / Tokyo+readmission / Seoul
│ │ ├── missing_visit_date_validation.py
│ │ ├── server_side_date_bypass.py # G-DATA-1
│ │ ├── past_date_booking.py # G-DATA-1
│ │ ├── enter_on_visit_date_no_submit.py
│ │ ├── duplicate_booking.py # G-DATA-2
│ │ ├── overlapping_appointments.py # G-DATA-2
│ │ └── saturation_booking.py # exploratory
│ └── Suite "History"
│ ├── view_history.py
│ └── empty_history.py
├── Campaign "Profile"
│ └── Suite "Profile access"
│ └── view_profile.py # G-SPEC-2 documente la placeholder
└── Campaign "User journeys"
└── Suite "Cross-feature flows"
├── book_and_verify_history.py
├── history_ordering.py # G-SPEC-1
├── sidebar_navigation.py
└── sidebar_login_navigation.pyCLAUDE.md#
« Smoke is structural, not lexical »#
Express the gate via
TestCycle.smoke_tests_campaigns=; never put “smoke” in a name string.
→ Le mot « smoke » ne doit pas apparaître dans les noms de tests / suites. La sémantique est portée par la structure Ocarina (smoke_tests_campaigns=), pas par un nom.
« Smoke is the minimum that proves the system is alive »#
A test belongs in smoke iff its failure makes the rest of the cycle uninformative.
→ Critère strict : un test n’est dans le groupe des smoke tests que si son échec invalide le reste.
Comme valid_login : sans pouvoir s’authentifier, on ne peut rien tester sur CURA.
« Each main campaign is feature-shaped, not method-shaped »#
→ Campagnes nommées par fonctionnalités (Authentication, Appointments, Profile, User journeys), pas par méthode (POST tests, GET tests).
« Default mode is fail-fast »#
→ Le mode par défaut des smoke tests suffit pour ce cycle de test. wait-for-all-smoke-tests est réservé aux cas où plusieurs smoke tests sont indépendants (cf. ocarina-example qui a 2 smoke campaigns).
--workers 3#
Never
--workers 1. Single-worker runs mask concurrency failures and diverge from CI. Always--workers 3(matchesai_proof_e2e.yml); CI is the canonical reference.
→ Discipline : ne jamais tester en single-worker.
- Un seul worker masque les races conditions.
- La CI tourne avec 3 workers ; on veut faire la même chose en local.
saturate_workers=Trueclone les tests pour atteindre N = 3 → exposition aux heisenbugs.