Modular YAML test composition: practical patterns you can reuse for readable, durable automated tests

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.

Why modular YAML matters more than readable YAML

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.

A mental model for modular tests

Think in three layers:

The examples that follow show how to implement that model in YAML.

Pattern: reusable flows as first-class modules

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:

  • Consistency: every test that needs authentication behaves the same way.
  • Lower maintenance: when your login UI changes, you edit one module.
  • Better collaboration: product and QA can review flows like documentation.

In Shiplight, this maps naturally to composing tests out of reusable YAML modules while keeping execution intent-based rather than bound to fragile selectors.

Pattern: parameterized templates for the same test, many variants

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.

Pattern: separate assertion bundles from interactions

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.

Pattern: environment and test data as explicit modules

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.

Pattern: compose suites by risk, not by org chart

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:

  • Smoke: a small set of end-to-end flows that must pass before merging.
  • Critical path: revenue and retention journeys, run on every deploy.
  • Deep regression: broader coverage, scheduled or nightly.
  • Change-focused suites: targeted runs triggered by pull requests and risk areas.

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.

What good modular YAML looks like in practice

Before you scale a suite, sanity-check your composition:

  • If a user journey appears in more than two tests, it should probably be a reusable flow.
  • If a test differs only by data, it should probably be a template instantiation.
  • If a UI expectation repeats across features, it should probably be an assertion bundle.
  • If people argue about what a test means, the module names are not intent-revealing enough.

The goal is not clever YAML. The goal is durable proof that the UI still supports the behaviors your users rely on.

Where Shiplight AI fits

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.