Modular YAML Test Composition: Practical Patterns and Examples for Maintainable Automation

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.

What modular means in YAML test design

A modular test suite is built from small, reusable building blocks:

  • Flows: repeatable user journeys like “log in,” “add to cart,” or “invite teammate.”
  • Assertions: reusable checks like “toast appears,” “route changed,” or “table contains row.”
  • Data setup: consistent fixtures for accounts, plans, feature flags, and seeded records.
  • Parameters: the small set of inputs that make a flow reusable across roles, locales, or plans.

The goal is simple: write each idea once, then compose it everywhere.

Pattern: YAML anchors and aliases for shared step blocks

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:

  • You keep “navigation hygiene” consistent (cookie banners, locale pickers, onboarding modals).
  • You can update a shared block once and reuse it in multiple places inside the file.
  • It stays highly readable for non-authors reviewing the test.

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.

Pattern: Composable flows as first-class modules

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.

Pattern: Parameterized templates for role and plan coverage

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.

Pattern: Reusable assertions that verify outcomes, not implementation

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.

Pattern: A thin test that reads like a spec

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.

Why modular YAML pairs naturally with Shiplight AI

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:

  • Intent-based execution helps keep your modules stable when structure shifts, labels change slightly, or elements move.
  • Self-healing automation reduces the “one UI tweak, 40 failures” pattern that makes shared flows feel risky.
  • Visual editing and AI Copilot make it practical to refine a shared flow once and confidently reuse it across the suite.
  • PR-driven test generation complements modular design by proposing new coverage in the same building-block vocabulary, instead of adding one-off scripts.

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.