03.05 — Declarative programming#

An Ocarina scenario describes, it doesn’t execute. That’s what makes it factorable, multipliable, readable.

Demonstration#

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!")),
    ),
]

This code executes nothing. It describes:

  • I take control of the homepage”,
  • I open + verify; on error log A, on success log B”,
  • I click the CTA; on error log C, on success log D”,
  • I take control of the next page”,
  • I verify it; logs”.

The Holy Book puts it explicitly (chapter “First scenarios”):

This style of writing is pure: it produces no immediate effect. Everything can be redeclared elsewhere, reorganized elsewhere, as long as the final chain matches what is expected.

Capabilities that flow from declarativity#

1. Aliasing#

click_confirm_cookies = drive_page(
    act(on_homepage, confirm_cookie_banner)
        .failure(log_error_with_current_url("Failed..."))
        .success(log_success_with_current_url_and_take_screenshot("Confirmed!"))
)

# … later …
[
    match_page(branches=[
        when(check.has_cookies_banner, name="cookies", then=[click_confirm_cookies]),
        when(check.has_not_cookies_banner, name="no cookies", then=[]),
    ]),
    drive_page(...),
]

→ We pull a drive_page into a variable and reuse it. Just a value.

2. Multiplication#

[
    drive_page(
        act(on_dashboard, click_btn)...,
        act(on_dashboard, verify_msg)...,
    ),
] * 5

→ Repeats the chain 5 times. Without laziness, we’d execute once and hold 5 refs to the same result; with it, we get 5 distinct executions.

3. Fragments#

test_with_fragments = create_selenium_test(
    name="...",
    test_scenario=lambda driver, logger: Scenario(test_chain=my_chain(driver, logger)),
    pre_test_scenarios_fragments=[login_without_otp_happy_path],     # ← fragment injected before
    post_test_scenarios_fragments=[verify_homepage],                 # ← fragment injected after
)

→ Fragments are concatenated by Test.spawn.

4. Data-driven#

multi_login_dataset = [
    MappingProxyType({"login": "any", "password": "figatellu"}),
    MappingProxyType({"login": "Napoleon", "password": "figatellu"}),
    ...
]

def _create_login_scenario(credentials):
    def _scenario(driver, logger):
        # closure capturing credentials
        return Scenario(test_chain=[
            drive_page(
                act(page, login_with(credentials))...
            ),
        ])
    return _scenario

multi_login_tests = [
    create_selenium_test(name=f"Login - {c['login']}", test_scenario=_create_login_scenario(c))
    for c in multi_login_dataset
]

→ Credentials dataset → list of tests. The _create_login_scenario factory applies to each entry. dataset × scenario_template = tests composition.

5. Stateless branching#

[
    match_page(branches=[
        when(check.has_cookies_banner, name="cookies", then=[click_confirm_cookies]),
        when(check.has_not_cookies_banner, name="no cookies", then=[]),
    ]),
    drive_page(act(on_homepage, do_next_thing)...),
]

→ Branch picked at runtime, scenario’s shape declared up front.

The imperative vs declarative distinction, applied#

The Holy Book mentions it (chapter “First feedbacks”):

“Explaining” imperative vs. declarative programming to me while spewing complete nonsense

ImperativeDeclarative
Sequence of instructions with state mutationDescription of what you want
The program does thingsThe program is a description
Hard to compose / reorderEasy to compose / reorder
Tests brittle to side effectsRobust tests (the test executes the description)

Ocarina’s DSL is strictly declarative at the scenario level. Execution is a separate event (runner.run() or chain_runner.run() called by TestExecutor).

Also from the Holy Book (chapter “First scenarios”):

The priority of test scenarios is their uniformity and simplicity. That’s all.

You don’t nest try/except in a scenario. You don’t store state between acts. You don’t define a helper_step() that does a bunch of things. The DSL grammar enforces declaration.

This property is central to AI collaboration. The Holy Book (chapter “First feedbacks”) keeps coming back to the same line:

Code is raw data. Auditable. Inspectable. A white box. Exactly what AI has known how to work with since its very beginnings.

A declarative scenario is raw data — it describes, no hidden logic. That’s what AI consumes and produces best.

Same reason the whole system (mypy strict, ruff ALL, pytest-mypy-plugins) maxes out static checks, including static typing: as much for humans as for LLMs.