Modular YAML Test Composition: Practical Patterns and Examples for Maintainable Automation
Updated on May 1, 2026
Updated on May 1, 2026
Modern UI test suites rarely fail because teams cannot write a test. They fail because teams cannot keep tests readable, reusable, and stable as the product changes.
That is exactly where modular test composition shines. When your tests are written in human-readable YAML, modularity is not just a developer ergonomics win. It is what makes automated testing collaborative across engineering, QA, product, and design. Done well, it turns a brittle pile of scripts into a living specification that stays close to user intent.
Shiplight AI was built for this reality: AI-native teams shipping fast, validating UI changes in real browsers, and keeping regression coverage high without turning test maintenance into a second job. Shiplight’s YAML-based test format supports variables, templates, reusable functions, and modular composition so your suite can scale without becoming a rewrite cycle.
Below are concrete composition patterns you can use to keep YAML tests clean and durable, with examples you can adapt to your own conventions. Syntax varies by runner. The examples are intentionally focused on structure and composability, not a single rigid schema.
A modular test suite is built from small, reusable building blocks:
The goal is simple: write each idea once, then compose it everywhere.
Plain YAML gives you a powerful reuse tool without any special runner features: anchors (&) and aliases (*). This is a clean way to standardize repeated step sequences inside a single file or across copied snippets.
# checkout.smoke.yaml
name: Checkout smoke
steps:
- &go_to_home
action: navigate
to: "/"
- &accept_cookies
action: click
target: "button"
text: "Accept"
- &search_product
action: fill
target: "input"
label: "Search"
value: "Socks"
- *go_to_home
- *accept_cookies
- *search_product
- action: click
target: "link"
text: "Socks, Merino Wool"
- action: click
target: "button"
text: "Add to cart"
- action: click
target: "button"
text: "Checkout"
Where this helps in practice:
Shiplight’s intent-based execution complements this style well because the reusable blocks can describe actions in user terms, rather than binding your suite to fragile selectors.
Anchors are great, but most teams eventually want flows that can be shared across files. This is where modular composition becomes a suite-level strategy: you define a flow once and reference it everywhere.
Conceptually, the structure looks like this:
# flows/login.yaml
flow: login
params:
email: null
password: null
steps:
- action: navigate
to: "/login"
- action: fill
label: "Email"
value: "${email}"
- action: fill
label: "Password"
value: "${password}"
- action: click
text: "Log in"
- assert:
kind: page_contains
text: "Welcome back"
Then compose it inside multiple tests:
# billing/update-card.yaml
name: Update billing card
uses:
- flow: login
with:
email: "${secrets.ADMIN_EMAIL}"
password: "${secrets.ADMIN_PASSWORD}"
steps:
- action: navigate
to: "/settings/billing"
- action: click
text: "Update card"
- action: fill
label: "Card number"
value: "4242 4242 4242 4242"
- action: click
text: "Save"
- assert:
kind: toast_visible
text: "Payment method updated"
The key design choice: keep flows opinionated about intent (what the user is doing), but parameterized about inputs (who, what plan, what workspace). That combination is what keeps modular suites from becoming a maze of near-duplicates.
Most UI regressions are role and entitlement bugs. The UI looks right for an admin but breaks for a member, or a feature appears on the wrong plan.
Instead of cloning tests, compose one template with a small parameter matrix:
# templates/invite-user.yaml
template: invite_user
params:
role: "Member"
email: null
steps:
- action: click
text: "Invite user"
- action: fill
label: "Email"
value: "${email}"
- action: select
label: "Role"
value: "${role}"
- action: click
text: "Send invite"
- assert:
kind: toast_visible
text: "Invitation sent"
Now reuse it:
# team/invites.regression.yaml
name: Team invites regression
cases:
- use: invite_user
with:
role: "Member"
email: "member_${run.id}@example.com"
- use: invite_user
with:
role: "Admin"
email: "admin_${run.id}@example.com"
This pattern is especially effective when paired with Shiplight’s cloud runners and CI/CD triggers, because you can run targeted matrices on pull requests and reserve broader coverage for scheduled runs.
Teams often modularize actions, but leave assertions ad hoc. That is a missed opportunity. Assertions are where consistency matters most, because they encode what “done” means.
A reusable assertion module might verify a success state across multiple workflows:
# asserts/success-toast.yaml
assertion: success_toast
params:
text: null
checks:
- assert:
kind: toast_visible
variant: "success"
text: "${text}"
- assert:
kind: toast_not_visible
timeout_ms: 10000
text: "${text}"
Why this matters: the second check (“it disappears”) catches UI states that get stuck and block the next interaction, a common source of flakiness and false passes.
Shiplight’s AI-powered assertions are designed to reduce the gap between “the DOM says it exists” and “the UI is actually correct,” which becomes increasingly important as component libraries and rendering logic evolve.
Once you have flows, templates, and assertions, the test file itself should become short and product-readable:
# onboarding/new-workspace.smoke.yaml
name: New workspace onboarding smoke
tags: [smoke, onboarding]
uses:
- flow: login
with:
email: "${secrets.SMOKE_EMAIL}"
password: "${secrets.SMOKE_PASSWORD}"
steps:
- step: create_workspace
with:
name: "Shiplight Smoke ${run.date}"
- step: complete_onboarding_checklist
- assert: workspace_home_visible
This is the real payoff of modular composition: a test that communicates intent clearly enough that reviewers can catch coverage gaps during code review, not after a production incident.
YAML readability is only half the story. The other half is maintenance economics.
Modular composition reduces duplication, but tests still break when UIs change. Shiplight is designed to absorb that change:
If you want a suite that scales, treat your YAML like a product artifact: modular, reviewed, and written in the language your team uses to talk about the UI. That is how automated testing becomes an accelerator, not a tax.