Modular YAML test composition: practical patterns you can reuse for readable, durable automated tests
Updated on April 12, 2026
Updated on April 12, 2026
This post uses example YAML to illustrate modular composition patterns. Exact keys and structure vary by tool. Shiplight AI supports a human-readable YAML format with variables, templates, reusable functions, and modular composition, so the patterns below map cleanly to how teams structure real Shiplight suites.
Human-readable YAML is a great starting point, but it is not the finish line. The real cost in end-to-end automation shows up later: duplicated steps, brittle selectors, and simple changes that require editing dozens of tests. Modular composition is how you keep tests readable while also keeping them maintainable.
For AI-native teams, modularity has a second benefit: it creates a stable contract between intent and execution. When a test is built from reusable, intention-revealing building blocks, you can evolve the UI without rewriting the suite. This aligns with Shiplight AI’s intent-based execution and self-healing approach: you keep tests focused on user behavior and let the platform handle the mechanics of locating and interacting with the UI.
Below are modular patterns that consistently hold up in real teams, expressed in a human-readable YAML style.
Think in three layers:
The examples that follow show how to implement that model in YAML.
Most teams start by copying and pasting a login sequence into every test. A better approach is to define a login flow once, then compose it into many scenarios.
flow: login
description: Sign in as a known user
params:
email: ${USER_EMAIL}
password: ${USER_PASSWORD}
steps:
- go_to: ${BASE_URL}/login
- fill:
field: Email
value: ${email}
- fill:
field: Password
value: ${password}
- click: Sign in
- expect:
url_contains: /app
- expect:
visible_text: Welcome
Then reuse it:
test: Change password from settings
tags: [critical_path, settings]
uses:
- flow: login
with:
email: ${SEED_USER_EMAIL}
password: ${SEED_USER_PASSWORD}
steps:
- click: Settings
- click: Security
- fill: { field: New password, value: ${NEW_PASSWORD} }
- click: Save
- expect: { visible_text: Password updated }
What this buys you:
In Shiplight, this maps naturally to composing tests out of reusable YAML modules while keeping execution intent-based rather than bound to fragile selectors.
Another common failure mode is duplicating an entire test just to vary a plan type, a locale, or a payment method. Templates prevent that by making data the variable, not the test.
template: checkout_happy_path
params:
plan: starter
payment_method: card
steps:
- go_to: ${BASE_URL}/pricing
- click: "Choose ${plan}"
- expect: { visible_text: Checkout }
- when: ${payment_method} == "card"
then:
- fill: { field: Card number, value: ${CARD_NUMBER} }
- fill: { field: Expiration, value: ${CARD_EXP} }
- fill: { field: CVC, value: ${CARD_CVC} }
- click: Pay now
- expect: { visible_text: Thanks for your purchase }
Instantiate it:
suite: Checkout coverage
cases:
- use_template: checkout_happy_path
with: { plan: starter, payment_method: card }
- use_template: checkout_happy_path
with: { plan: pro, payment_method: card }
This structure is especially effective when paired with Shiplight’s AI-powered assertions and visual editor: your template captures the stable intent, and you can refine the assertions once at the source rather than per copy.
When tests fail, you want fast diagnosis: did the action fail, or did the UI fail to render the right state? A clean way to do this is to modularize assertions into named checks that can be reused across flows.
check: navbar_signed_in
steps:
- expect: { visible_text: Dashboard }
- expect: { visible_text: Settings }
- expect: { visible_text: Sign out }
Use it anywhere:
test: Navbar shows authenticated navigation
uses:
- flow: login
steps:
- run_check: navbar_signed_in
The benefit is not just reuse. It also creates a shared vocabulary across engineering, product, and QA: “Run the signed-in navbar check” is unambiguous and reviewable.
Teams often hide configuration inside CI variables and hope everyone remembers what is set where. A more robust approach is to declare environment and seed data explicitly in YAML, then import it consistently.
vars:
BASE_URL: https://staging.example.com
SEED_USER_EMAIL: qa-staging@example.com
SEED_USER_PASSWORD: ${SECRET:STAGING_SEED_PASSWORD}
test: Staging smoke
imports:
- config/env.staging.yaml
uses:
- flow: login
with:
email: ${SEED_USER_EMAIL}
password: ${SEED_USER_PASSWORD}
steps:
- expect: { visible_text: Dashboard }
This pattern reduces “works on my machine” failures because the test describes its dependencies in the same place it describes its intent.
Modular composition is not only about code reuse. It also determines whether your test suite produces signal quickly.
A pragmatic suite strategy looks like this:
Shiplight’s cloud runners, CI/CD integrations, and dashboards fit naturally here: once tests are modular, you can run the right slices at the right time without turning every pipeline into an endurance event.
Before you scale a suite, sanity-check your composition:
The goal is not clever YAML. The goal is durable proof that the UI still supports the behaviors your users rely on.
Shiplight AI is built for teams that want to verify UI changes in real browsers during development, without paying the ongoing tax of brittle automation. Modular YAML composition is the structure that makes that possible at scale: it keeps intent readable, keeps change localized, and gives your team a shared language for quality.