Stop Duplicating User Flows: Modular YAML Tests That Scale

Updated on April 25, 2026

The fastest way to make a test suite unreadable is to copy a checkout flow five times and change two lines in each file.

Teams do this because it feels practical at first. A login test becomes a billing test, then an upgrade test, then a cancellation test. Each one starts with the same account setup, the same sign-in steps, the same navigation, and the same assertions around page readiness. A month later, a button label changes and six tests fail for the same reason.

This is exactly where modular composition earns its keep.

In a human-readable YAML test format, the goal is not clever abstraction. The goal is to separate what repeats from what varies so tests stay readable in pull requests and cheap to maintain. Shiplight AI lives in that world, and the same design principle applies no matter which runner sits underneath: compose tests from small, named blocks that mirror product intent.

The right unit of reuse is a user action

Most brittle test reuse starts too low in the stack. Reusing selectors is not modularity. Reusing user actions is.

Bad reuse looks like this:

click: ".btn-primary"

type: "#email"

assertVisible: ".success"

Useful reuse looks like this:

flows:

login:

- visit: "/login"

- fill:

email: "${user.email}"

password: "${user.password}"

- click: "Sign in"

- assert:

text: "Dashboard"

add_item_to_cart:

- visit: "/products"

- click: "${product.name}"

- click: "Add to cart"

- assert:

text: "Added to cart"

That shift matters. A reusable block should describe a business action a reviewer can understand without knowing the DOM.

Compose tests from shared flows and local intent

A good modular test keeps the common path centralized and the reason for the test local.

test: "Pro user can upgrade seat count"

vars:

user:

email: "owner@example.com"

password: "secret123"

plan: "Pro"

seats: 12

use:

  • login
  • open_billing_page

steps:

  • click: "Manage plan"
  • select:

    field: "Plan"

    option: "${plan}"
  • fill:

    seats: "${seats}"
  • click: "Confirm upgrade"

assert:

  • text: "Plan updated"
  • text: "${seats} seats"

The use section holds the shared setup. The steps section holds the thing this test is actually proving. That boundary keeps the file honest. If the test title is about upgrading seat count, the body should not spend 20 lines re-explaining login.

Parameterize data, not behavior

A common mistake is building one monster flow with conditionals for every scenario. That makes the YAML compact but the suite harder to reason about.

A better pattern is to keep behavior fixed and swap inputs.

template: "Checkout works for payment methods"

cases:

  • name: "Visa"

    payment_method: "Visa ending in 4242"
  • name: "PayPal"

    payment_method: "PayPal"
  • name: "Gift card"

    payment_method: "Gift card"

use:

  • login
  • add_item_to_cart

steps:

  • click: "Checkout"
  • choose: "${payment_method}"
  • click: "Place order"

assert:

  • text: "Order confirmed"

This is where YAML shines. Reviewers can see the matrix immediately. The behavior is stable. The test data is what changes. That usually produces cleaner failure analysis too.

Keep assertions modular too

Teams often modularize setup and forget that assertions repeat just as much.

If every account settings test needs to confirm save success, persisted values, and no error banner, extract that as a shared assertion block.

assertions:

settings_saved:

- text: "Changes saved"

- notVisible: "Something went wrong"

- fieldValue:

field: "Display name"

value: "${profile.display_name}"

Then a focused test becomes much smaller:

test: "User can update display name"

vars:

profile:

display_name: "Jordan Lee"

use:

  • login
  • open_settings_page

steps:

  • fill:

    display_name: "${profile.display_name}"
  • click: "Save"

assert:

  • use: settings_saved

That pattern does two things. It removes duplication, and it makes your quality standard explicit.

The best YAML suites read like a map, not a script dump

A healthy modular suite usually has four layers:

  • Fixtures for data and environment setup
  • Flows for reusable user actions
  • Templates for scenario families driven by inputs
  • Test files for one specific claim about product behavior

The discipline is simple: if a block is reused, name it after user intent; if a block is unique, keep it in the test. Do not chase maximum reuse. Chase minimum confusion.

That is the real promise of human-readable YAML in automated testing. Not that non-engineers can technically open the file, but that the file still makes sense three months later when the product changes and the team needs to decide what actually broke.