04.01 — Stratégie « dehors comme un utilisateur »#

C’est la posture documentée dès le conftest.py des tests scénarios. Aucun test ne triche en regardant les internes.

tests/scenarios/conftest.py#

ComposantRôle
FakeDriver (dataclass)Driver minimaliste : title, disposed. Aucune méthode Selenium.
make_built_driver()Factory (FakeDriver, dispose), soit la signature attendue par WebDriversPool.
make_pool(max_size=2)WebDriversPool[FakeDriver] prêt à l’emploi.
RecordingPOM(POMBase)POM qui consigne ses appels et peut être configuré pour lever.
acting(pom, step)ActionSuccess[RecordingPOM] complet (failure+success en no-op).
scenario_of("ok", "ok2")TestScenario[FakeDriver] à N steps.
failing_scenario(step="boom", exc=...)Scénario à 1 step qui lève.
make_test(name, scenario=..., test_id=..., skipped=False)Test[FakeDriver]
make_suite(name, tests, pool=..., transient_errors=..., max_retries_per_test=...)TestSuite[FakeDriver]
make_campaign(name, suites, max_workers=1)TestCampaign[FakeDriver]
make_cycle(name="cycle", campaigns=..., smoke=..., mode=...)TestCycle[FakeDriver]
run_chain(runner)Helper qui exécute un ChainRunner et retourne le ActionChain.

FakeDriver#

@dataclass
class FakeDriver:
    title: str = "fake"
    disposed: bool = False

C’est tout. Le cœur d’Ocarina n’appelle aucune API Selenium.

Tout ce qui est appelé sur le driver est appelé par le POM (que l’utilisateur écrit).

Le framework lui-même ne fait que :

  • L’acquérir via WebDriversPool.acquire().
  • Le passer au scénario / aux watchers.
  • Le disposer à la fin.

Donc un FakeDriver sans rien est suffisant pour tester l’orchestration.
C’est la preuve par les tests que le framework est agnostique.

RecordingPOM#

@dataclass
class RecordingPOM(POMBase):
    calls: list[str] = field(default_factory=list)
    raise_on: set[str] = field(default_factory=set)
    raises_with: dict[str, Exception] = field(default_factory=dict)
    _title: str = "Recording page"

    def verify(self, *, timeout=None) -> "RecordingPOM":
        return self._do("verify")

    def get_current_title(self) -> str:
        return self._title

    def step(self, name: str) -> "RecordingPOM":
        return self._do(name)

    def _do(self, name: str) -> "RecordingPOM":
        self.calls.append(name)
        if name in self.raise_on:
            raise self.raises_with.get(name, RuntimeError(f"{name} failed"))
        return self

C’est un spy.

  • Enregistre chaque appel dans self.calls. Permet d’asserter ce qui a été appelé, dans quel ordre.
  • Peut lever à la demande : RecordingPOM(raise_on={"boom"}, raises_with={"boom": ValueError("nope")}).
  • Retourne self sur chaque appel (fluent chaining).
def test_short_circuit_after_failure() -> None:
    pom = RecordingPOM(raise_on={"second"})
    runner = drive_page(
        acting(pom, "first"),
        acting(pom, "second"),
        acting(pom, "third"),
    )
    chain = runner.run()
    assert chain.has_failed()
    assert pom.calls == ["first", "second"]    # ✅ "third" jamais appelé

acting(pom, step)#

def acting(pom: RecordingPOM, step: str) -> Any:
    return (
        create_act(pom, lambda p: p.step(step))
        .failure(lambda exc: None)
        .success(lambda: None)
    )

→ Un ActionSuccess[RecordingPOM] complet, avec handlers no-op. Utilisable directement dans drive_page(...).

make_*#

make_test, make_suite, make_campaign, make_cycle instancient le framework comme un utilisateur. Tous prennent des valeurs par défaut raisonnables (MutedLogger, make_pool(), max_workers=1, etc.) pour ne pas polluer les tests avec du boilerplate.

On teste l’API, pas l’interne.

Absence de mock du framework#

Aucun test n’utilise unittest.mock. Quelques tests unittest.mock apparaissent ponctuellement pour mocker du time ou mocker un appel externe, mais jamais pour mocker une classe du framework.

C’est la garantie que les tests valident le comportement réel du framework, pas une supposition sur lui.

@allure.*#

@allure.epic("Railway / action chain")
@allure.feature("Action chain")
@allure.tag("act", "error-handling")
@allure.severity(allure.severity_level.CRITICAL)
@allure.label("layer", "unit")
@allure.title("A raising action is caught into Fail; the failure handler fires with the exception")
def test_raising_act_is_captured_as_fail() -> None: ...
DécorateurSens
@allure.epicPlus haut niveau de regroupement (Allure)
@allure.featureSous-niveau (feature)
@allure.tagTags libres (act, error-handling, retry, etc.)
@allure.severityTRIVIAL, MINOR, NORMAL, CRITICAL, BLOCKER
@allure.label("layer", "unit")Label custom — distingue unitintegratione2e
@allure.titleTitre humain, lisible dans le rapport

Rapport allure (tests du framework)

# ruff: noqa: S101#

# ruff: noqa: S101
"""Railway and action-chain behavior."""

S101 interdit l’usage d’assert (par défaut, en production code). Dans les tests, c’est inévitable.
# ruff: noqa: S101 désactive cette règle pour tout le fichier.