Extensibilité
ValidationChain
Utilisable dans les POMs, validate permet d'exprimer des invariants sous forme de chaînes. L'exécution est différée : il faut appeler .execute() explicitement.
Le résultat expose is_valid, errors et validated_values. Il est inerte par défaut. .raise_if_invalid() remonte l'exception si besoin.
validate(checkbox.is_selected(), name="checkbox_is_selected").assert_that(
is_truthy, msg="Couldn't select the OTP checkbox."
).execute().raise_if_invalid()Chaînage d'invariants
Plusieurs assertions sur une même valeur :
validate(unsafe_min_date, name="cached_min_date")
.assert_that(is_str)
.assert_that(is_iso_utc_date_string).execute().raise_if_invalid()Chaînage de validations
Plusieurs validations sur des valeurs différentes :
chain_validations(
validate(unsafe_username, name="cached_username").assert_that(is_str),
validate(unsafe_min_date, name="cached_min_date")
.assert_that(is_str)
.assert_that(is_iso_utc_date_string),
).execute().raise_if_invalid()Invariants réutilisables
Pour factoriser une validation récurrente, créer un Invariant Validator :
def _workers_amount_chain(
chain: ValidationStartBlock[int],
value: int,
) -> ValidationAssertBlock[int]:
msg = f"Value Error: Number of workers must be at least 1 (got: {value})."
return chain.assert_that(is_positive, msg=msg).assert_that(is_not_zero, msg=msg)
def validate_workers_amount(
*, workers_amount: int, name: str
) -> ValidationAssertBlock[int]:
"""Validate that workers amount is at least 1."""
return FrameworkInvariantValidator.create(
workers_amount, name, _workers_amount_chain
)
# * ...
validate_workers_amount(
workers_amount=max_workers, name="max_workers"
).execute().raise_if_invalid()Convention : FrameworkInvariantValidator.create pour les invariants techniques, BusinessInvariantValidator.create pour le métier.
Assertions personnalisées
Sans argument :
def is_str(value: Any) -> None:
if not isinstance(value, str):
msg = "Expected value to be string."
raise InvariantViolationError(msg)Avec argument :
def is_equal_to(cmp: Any) -> Predicate[Any]:
def unwrapped(value: Any) -> None:
if value != cmp:
msg = f"{value} is not equal to {cmp}."
raise InvariantViolationError(msg)
return unwrappedType safety
Le type checker détecte les assertions incompatibles avec le type de la valeur :
validate("lol", name="n").assert_that(is_positive)
# error: Argument 1 to "assert_that" of "ValidationStartBlock" has incompatible type "Callable[[float], None]"; expected "Callable[[str], None]"Success et failure
.success et .failure prennent chacun un effet à exécuter.
L'exemple canonique implémente plusieurs handlers : log simple d'erreur, log avec URL courante, log de succès, et log de succès avec screenshot (+ URL).
def _append_current_url_in_msg(msg: str, driver: WebDriver) -> str:
try:
driver_healthcheck(driver)
extended_msg = f"{msg}\nCurrent URL: {driver.current_url}"
except DriverDiedError:
extended_msg = f"{msg}\nThe WebDriver is down, can't provide the current URL."
return extended_msg
def create_just_log_error(*, logger: ILogger) -> Callable[[str], FailureHandler]:
return lambda msg: lambda exc: logger.error(msg, exc=exc)
def create_log_error_with_current_url(
*, logger: ILogger, driver: WebDriver
) -> Callable[[str], FailureHandler]:
def unwrapped(msg: str) -> FailureHandler:
def _log_error_with_url_effect(exc: Exception) -> None:
extended_msg = _append_current_url_in_msg(msg, driver)
return create_just_log_error(logger=logger)(extended_msg)(exc)
return _log_error_with_url_effect
return unwrapped
def create_just_log_success(*, logger: ILogger) -> Callable[[str], SuccHandler]:
def unwrapped(msg: str) -> SuccHandler:
def _log_effect() -> None:
logger.success(msg)
return _log_effect
return unwrapped
def create_log_success_and_take_screenshot(
*, logger: ILogger, driver: WebDriver
) -> Callable[[str], SuccHandler]:
def unwrapped(msg: str) -> SuccHandler:
def _log_and_take_screenshot_effect() -> None:
performed_dependent_effect = create_just_log_success(logger=logger)(msg)()
take_screenshot(driver=driver, logger=logger, category="SUCCESS")
return performed_dependent_effect
return _log_and_take_screenshot_effect
return unwrapped
def create_log_success_with_current_url_and_take_screenshot(
*, logger: ILogger, driver: WebDriver
) -> Callable[[str], SuccHandler]:
def unwrapped(msg: str) -> SuccHandler:
def _log_success_with_url_and_take_screenshot_effect() -> None:
return create_log_success_and_take_screenshot(logger=logger, driver=driver)(
_append_current_url_in_msg(msg, driver)
)()
return _log_success_with_url_and_take_screenshot_effect
return unwrappedD'autres handlers sont envisageables :
create_log_error_with_retry_hint: signale une transient error et donc la possibilité de flakiness,create_log_error_and_send_alert: envoie un webhook à l'échec, sans polluer le test,create_log_success_and_record_timing: capture un timestamp de fin pour mesurer la durée d'un step (à combiner avecon_run_effectdecreate_act),- Etc.
La création d'un combinateur est également envisageable.
Plugins
bootstrap permet de lancer des plugins post-exécution basés sur les résultats du cycle de test. Par exemple, generate_docx_proof parcourt l'arborescence de logs et génère un document Word (preuve de test) par cas de test, en insérant les captures d'écran et en convertissant les dates UTC en heure locale.
Le principe : les plugins réassemblent les artefacts générés en cours de route sous une forme différente. Un plugin pour créer un rapport sous forme de tableau de bord web serait par exemple tout à fait envisageable.
Grammaire extensible
La grammaire des scénarios de test repose sur un seul type : ChainRunner[T]. Un scénario est une list[ChainRunner] exécutée séquentiellement, court-circuitée au premier échec. drive_page n'est qu'une fine enveloppe autour de chain_actions, qui construit un ChainRunner. N'importe quelle fonction renvoyant un ChainRunner s'insère sans toucher au framework.
match_page a été ajouté après coup pour gérer les pages à état variable (banners optionnels, A/B tests, pages de maintenance...) : elle évalue des conditions dans l'ordre et exécute la première branche correspondante.
Autre exemple envisageable : skip_if, qui court-circuiterait volontairement une portion du scénario sur une condition sans échouer (retournerait un Ok neutre), utile pour des étapes optionnelles selon l'environnement ou les données de test.
La seule contrainte du point d'extension : retourner un ChainRunner.

Bon travail !
À une prochaine fois, lecteur Mojo.
"Le style, pour l'écrivain aussi bien que pour le peintre, est une question non de technique, mais de vision."
