Первые сценарии
Конец недопустимых состояний
... Сделайте недопустимые состояния непредставимыми.
Мы рассмотрим, как act, drive_page и match_page работают при написании тестовых сценариев с Ocarina.
Act и drive_page
Канонический пример
Давайте начнём с примера, который мы постепенно сломаем:
def go_from_homepage_to_book_call_page_with_the_cta(driver: WebDriver, logger: ILogger):
"""Open and verify my homepage."""
on_homepage = Homepage(driver=driver)
on_book_a_call_page = BookCallPage(driver=driver)
just_log_error = create_just_log_error(logger=logger)
just_log_success = create_just_log_success(logger=logger)
log_success_with_current_url_and_take_screenshot = (
create_log_success_with_current_url_and_take_screenshot(
logger=logger, driver=driver
)
)
return [
drive_page(
act(on_homepage, open_then_verify_homepage)
.failure(just_log_error("Failed to reach the homepage..."))
.success(
log_success_with_current_url_and_take_screenshot(
"On the homepage!"
)
),
act(on_homepage, click_book_call_page_cta)
.failure(just_log_error("Failed to click on the 'Book a call' CTA..."))
.success(just_log_success("Clicked on the 'Book a call' CTA!")),
),
drive_page(
act(on_book_a_call_page, verify_book_call_page)
.failure(just_log_error("Failed to verify the 'Book a call' page..."))
.success(
log_success_with_current_url_and_take_screenshot(
"On the 'Book a call' page!"
)
),
),
]
test_homepage_book_a_call_cta = create_selenium_test(
name="Go from homepage to book a call page, clicking the CTA",
test_scenario=lambda driver, logger: Scenario(
test_chain=go_from_homepage_to_book_call_page_with_the_cta(driver, logger)
),
)drive_page выражает, что мы берём управление одной страницы.
Каждый переход становится явным через открытие нового drive_page.
Внутри, act выражает действие, выпущенное на этой странице: это test step. drive_page вариативен: он принимает столько вызовов act, сколько нужно, и запятая между каждым становится AND:
Открыть, затем проверить домашнюю страницу. И кликнуть CTA. Мы переходим на страницу: проверить, что мы на странице book-a-call.
Иммунная система
Давайте попытаемся вызвать verify_book_call_page на homepage:
act(on_homepage, verify_book_call_page)
# error: Argument 2 to "act" has incompatible type "Callable[[BookCallPage], BookCallPage]"; expected "Callable[[Homepage], Homepage]"Действие несовместимо с его целью. Эта программа не компилируется.
Давайте забудем .success:
drive_page(
act(on_book_a_call_page, verify_book_call_page)
.failure(just_log_error("Failed to verify the 'Book a call' page..."))
)
# error: Expected type 'ActionSuccess[TPOM ≤: POMBase]', got 'ActionFailure[BookCallPage]' insteadДавайте разместим .success сразу после act:
drive_page(
act(on_book_a_call_page, verify_book_call_page)
.success(
log_success_with_current_url_and_take_screenshot(
"On the 'Book a call' page!"
)
),
),
# error:
# "ActionStart[BookCallPage]" has no attribute "success"
# Unresolved attribute reference 'success' for class 'ActionStart'Давайте поменяем местами .success и .failure:
drive_page(
act(on_book_a_call_page, verify_book_call_page)
.success(
log_success_with_current_url_and_take_screenshot(
"On the 'Book a call' page!"
)
)
.failure(just_log_error("Failed to verify the 'Book a call' page...")),
),
# error:
# "ActionStart[BookCallPage]" has no attribute "success"
# Unresolved attribute reference 'success' for class 'ActionStart'Давайте объединим неоднородные вызовы act внутри одного drive_page:
drive_page(
act(on_homepage, open_then_verify_homepage)
.failure(just_log_error("Failed to reach the homepage..."))
.success(
log_success_with_current_url_and_take_screenshot(
"On the homepage!"
)
),
act(on_homepage, click_book_call_page_cta)
.failure(just_log_error("Failed to click on the 'Book a call' CTA..."))
.success(just_log_success("Clicked on the 'Book a call' CTA!")),
act(on_book_a_call_page, verify_book_call_page) # <- [!]
.failure(just_log_error("Failed to verify the 'Book a call' page..."))
.success(
log_success_with_current_url_and_take_screenshot(
"On the 'Book a call' page!"
)
),
),
# error: Expected type 'ActionSuccess[Homepage]' (matched generic type 'ActionSuccess[TPOM ≤: POMBase]'), got 'ActionSuccess[BookCallPage]' insteadНИ ОДНА из этих фантазий не компилируется. Ocarina против умного ослабления.
Многие команды спорят о coding styles.
Ocarina более ясна: если стиль не соблюдается, это не lint ошибка, это не предупреждение. Это не компилируется.
Ocarina применяет один и тот же шаблон для всех, и эти ошибки непосредственно появляются в редакторе через mypy: мгновенная обратная связь.
Цель — как можно раньше закрыть экзистенциальные вопросы. Вот как Ocarina быстро отправит всех slipologists прямо в r/AntiWork. ✈️
Приоритет тестовых сценариев — их однородность и простота. Это всё.
match_page
match_page обрабатывает ситуации, когда страница может быть отображена по-разному.
Давайте начнём с принципа matchers:
@final
class PageWithCookiesBannerMatchers:
"""Drive nuts anybody with this page or use matchers."""
def __init__(self, *, driver: WebDriver) -> None:
"""Initialize helper."""
self._driver = driver
def has_cookies_banner(self) -> bool:
"""Quickly verify if the cookies banner is displayed."""
timeout = min(get_timeout(), 5)
try:
WebDriverWait(self._driver, timeout).until(
ec.visibility_of_element_located(
(By.CSS_SELECTOR, '[data-testid="cookies-banner"]')
)
)
except TimeoutException:
return False
return True
def has_not_cookies_banner(self) -> bool:
"""Quickly verify if the cookies banner is NOT displayed."""
timeout = min(get_timeout(), 5)
try:
WebDriverWait(self._driver, timeout).until(
ec.invisibility_of_element_located(
(By.CSS_SELECTOR, '[data-testid="cookies-banner"]')
)
)
except TimeoutException:
return False
return TrueMatcher минимально проверяет, верно ли что-то, как можно быстрее.
⚠️ Тем не менее, избегайте хвататься за сырой
.find_element(s): это прямая дорога к Selenium flakiness.
Ограничение в 5 секунд не оказывает значительного влияния на горизонтально масштабируемую батарею тестирования: это не то, о чём стоит беспокоиться здесь. Также не рекомендуется маскировать verify как matcher: это два разных инструмента.
Использование в сценарии:
on_homepage = Homepage(driver=driver)
check_that_page = PageWithCookiesBannerMatchers(driver=driver)
# * ...
[
match_page(
branches=[
when(
check_that_page.has_cookies_banner,
name="Has cookies banner",
then=[
drive_page(
act(on_homepage, confirm_cookie_banner)
.failure(
log_error_with_current_url(
"Failed to click on the cookies banner's confirm button..."
)
)
.success(
log_success_with_current_url_and_take_screenshot(
"Clicked on the cookies banner's confirm button!"
)
)
)
],
),
when(
check_that_page.has_not_cookies_banner,
name="Has NOT cookies banner",
then=[],
),
],
logger=create_matching_logger("terminal"), # <- [!] If you want debug logs
),
drive_page(
act(on_homepage, ...)
.failure(...)
.success(...)
),
]match_page находится на одном уровне с drive_page и компонуется так же. Его команда then ожидает цепь вызовов drive_page или match_page. Ветви определяются с помощью when.
match_page и when были добавлены поздно в Ocarina: Igoristan был настолько непредсказуем, что вариант использования стал очевидным.
Их интеграция была простой, доказательство гибкости грамматики: другие аналогичные структуры вполне могут следовать.
Повторения
Чтобы повторить цепь тестирования (например, чтобы протестировать несколько попыток несанкционированного доступа), просто умножьте список:
[
drive_page(
act(on_dashboard_welcome_page, click_on_go_to_nested_page_btn)
.failure(
just_log_error("Failed to click on the go-to-nested-page button...")
)
.success(just_log_success("Clicked on the go-to-nested-page button!")),
act(on_dashboard_welcome_page, verify_missing_otp_msg_is_displayed)
.failure(
just_log_error(
"Failed to find the missing OTP auth message...",
)
)
.success(
log_success_with_current_url_and_take_screenshot(
"Found the missing OTP auth message!"
)
),
),
] * 5 # <- [!]Фрагменты
Фрагмент — это функция (driver, logger) -> TestChain, которую можно внедрить до или после основной цепи, через pre_test_scenarios_fragments и post_test_scenarios_fragments.
Например, login_without_otp_happy_path — это фрагмент:
def login_without_otp_happy_path(driver: WebDriver, logger: ILogger):
"""Verify that we can connect without OTP."""
on_dashboard_login_page = DashboardLoginPage(driver=driver)
on_dashboard_welcome_page = DashboardWelcomePage(driver=driver)
# * ...
return [
drive_page(
act(on_dashboard_login_page, open_dashboard_login_page)
.failure(just_log_error("Failed to open the dashboard login page..."))
.success(just_log_success("Opened the dashboard login page!")),
# * ...
),
# * ...
]Внедрение в начале:
test_cant_access_the_protected_page_without_otp_using_the_ui = create_selenium_test(
name="Can't access the protected page without OTP (using the UI)",
test_scenario=lambda driver, logger: Scenario(
test_chain=dashboard_access_to_protected_page_without_otp_using_the_ui(
driver, logger
)
),
pre_test_scenarios_fragments=[login_without_otp_happy_path], # <- [!]
)Внедрение в конце:
test_dashboard_login_page_back_to_igoristan_button = create_selenium_test(
name="Use the go back to Igoristan button",
test_scenario=lambda driver, logger: Scenario(
test_chain=just_go_back_to_igoristan(driver, logger)
),
post_test_scenarios_fragments=[verify_homepage], # <- [!]
)Оба параметра можно комбинировать, и каждый принимает список фрагментов, внедряемых в указанном порядке.
Псевдонимы
Сценарии могут стать тяжёлыми.
Поскольку всё декларативно, пользователь волен создавать псевдонимы:
on_homepage = Homepage(driver=driver)
check_that_page = PageWithCookiesBannerMatchers(driver=driver)
click_confirm_cookies = drive_page(
act(on_homepage, confirm_cookie_banner)
.failure(
log_error_with_current_url(
"Failed to click on the cookies banner's confirm button..."
)
)
.success(
log_success_with_current_url_and_take_screenshot(
"Clicked on the cookies banner's confirm button!"
)
)
)
# * ...
[
match_page(
branches=[
when(
check_that_page.has_cookies_banner,
name="Has cookies banner",
then=[click_confirm_cookies], # <- [!]
),
when(
check_that_page.has_not_cookies_banner,
name="Has NOT cookies banner",
then=[],
),
],
logger=create_matching_logger("terminal"), # <- [!] If you want debug logs
),
drive_page(
act(on_homepage, ...)
.failure(...)
.success(...)
),
]Любое значение можно сделать псевдонимом и переиспользовать.
Это написание чистое: оно не производит немедленного эффекта.
Всё можно переобъявить в другом месте, переорганизовать в другом месте — лишь бы итоговая цепь соответствовала ожидаемому.

Отличная работа!
До скорой встречи, читатель Mojo.
"Совершенство достигается не тогда, когда нечего больше добавить, а когда нечего больше убрать."
