02.06 — Scenario[Driver]#
Fichier source :
src/ocarina/custom_types/scenario.py
Dataclass#
@final
@dataclass(frozen=True)
class Scenario[Driver]:
test_chain: TestChain
setup: TestSetup = field(default=None)
teardown: TestTeardown = field(default=None)
watchers: TestWatchers[Driver] | None = field(default=None)# src/ocarina/custom_types/test_components.py
type TestChain = Sequence[ChainRunner[Any]]
type TestSetup = Effect | None
type TestTeardown = Effect | None
type TestWatchers[Driver] = Sequence[Watcher[Driver]] | NoneCycle de vie (per attempt)#
1. setup() — optionnel, Effect libre (DB, API, …)
→ lève : skip de test_chain, jump à teardown,
return Outcome(setup_failed=True, should_retry=True)
→ ok : continue à test_chain
2. test_chain — la chaîne réelle (Sequence[ChainRunner])
(les watchers tournent pendant ce temps)
3. teardown() — optionnel, toujours exécuté
→ lève : log warning + ignore (n'affecte pas le verdict)
Si TOUTES les tentatives lèvent au setup :
→ test marqué SKIPPED (pas FAILED)
→ log warning « setup keeps failing »setup et teardown#
setup et teardown sont driver-free et injectionless by design.
C’est documenté dans le docstring du module :
They are plain
Effects —() -> None. They are meant for infrastructure concerns: seeding a database, calling an API, cleaning up state. Selenium belongs intest_chain. Whatever context they need (logger, driver, config) must be captured in the closure at scenario construction time.
def my_scenario(driver: WebDriver, logger: ILogger) -> Scenario[WebDriver]:
return Scenario(
setup=lambda: seed_test_user(logger=logger), # logger capturé
teardown=lambda: delete_test_user(logger=logger), # logger capturé
test_chain=[...],
)- Pas de couplage entre infrastructure et POMs. Le setup d’une DB n’a pas à connaître Selenium.
- Symétrie avec les
Watcher.callback: eux aussi reçoivent leur contexte via closure (leWatcherinjectedriver,logger,take_screenshotau moment destart()). - Testabilité : on peut tester un setup en isolation, sans le wrapper Selenium.
test_chain#
type TestChain = Sequence[ChainRunner[Any]]C’est exactement ce qu’on construit en retournant [drive_page(...), drive_page(...), match_page(...), drive_page(...)]. Chaque élément est exécuté séquentiellement par _run_chain (cf. 05-orchestration/02-test-executor.md).
Nuance : ChainRunner[Any] (pas ChainRunner[TPOM]). C’est parce qu’une test_chain peut mélanger des drive_page sur différentes pages (HomePage, LoginPage, DashboardPage…) et des match_page (qui retournent ChainRunner[Any]). L’Any est le ramasse-tout qui rend la séquence hétérogène typable.
watchers#
type TestWatchers[Driver] = Sequence[Watcher[Driver]] | NoneLes watchers sont des daemon threads qui observent le navigateur en parallèle de la chaîne, et qui reportent les frictions détectées via watcher.report(...). Voir 07-watcher.md
- Démarrés juste avant la chaîne, arrêtés juste après. Strictement scopés à
test_chain. Ils ne voient pas lesetupni leteardown. - Un thread par watcher.
- Un logger scopé par watcher :
(*taxonomy[:-1], "<test_name> - <watcher_name>"). Donne un fichier.logau même niveau que celui du test.
Exemple#
from ocarina.custom_types.scenario import Scenario
def my_scenario(driver: WebDriver, logger: ILogger) -> Scenario[WebDriver]:
page = MyPage(driver=driver)
return Scenario(
setup=lambda: seed_test_user(logger=logger),
test_chain=[
drive_page(
act(page, open_page)
.failure(log_error("Failed to open..."))
.success(log_success("Opened!")),
),
drive_page(
act(page, verify_page)
.failure(log_error("Failed to verify..."))
.success(log_success("Verified!")),
),
],
teardown=lambda: delete_test_user(logger=logger),
watchers=[
MyWatcher(...),
],
)Pourquoi frozen=True et @final#
@final + @dataclass(frozen=True) :
| Propriété | Conséquence |
|---|---|
frozen=True | Impossible de muter scenario.test_chain = [...] après création |
@final | Impossible d’hériter (class MyScenario(Scenario[WebDriver])) |
| Construction par kwargs implicite (dataclass) | API stable |
Un Scenario est une valeur.
TestRunner#
Test.spawn(driver, logger) retourne un TestRunner qui agrège :
chain_runners(concaténationpre + scenario.test_chain + post),setup(vient descenario.setup),teardown(vient descenario.teardown),watchers(vient descenario.watchers),skipped(vient deTest._skipped).
Donc : ce qui est dans le Scenario est strictement le scénario. Les fragments pre/post sont ajoutés autour par spawn.
Test associé#
tests/scenarios/test_test_suite.py contient un test qui passe par un scenario avec setup+teardown :
@allure.title("setup that fails on first attempt, succeeds on retry, then test passes")
def test_setup_retries_then_test_passes() -> None:
...