Skip to content

Instantly share code, notes, and snippets.

@ViniciusResende
Created November 6, 2025 13:56
Show Gist options
  • Select an option

  • Save ViniciusResende/e26baf208717106ae6e02a223768177f to your computer and use it in GitHub Desktop.

Select an option

Save ViniciusResende/e26baf208717106ae6e02a223768177f to your computer and use it in GitHub Desktop.

Evolving Our Test Architecture: A Layered BDD Approach

To support the planned growth of our test suite and ensure it remains "mindful," scalable, and maintainable, we will evolve its architecture.

Our goal is to create a highly readable and reusable structure that separates behavioral definitions from implementation logic. We will achieve this by integrating Behavior-Driven Development (BDD) and introducing a layered-action architecture.

1. Adopting BDD with Cucumber

To make our tests understandable for all stakeholders (including product owners) and to link them directly to our Software Testing Protocol, we will adopt Gherkin/Cucumber. Playwright can be integrated with a Cucumber runner (@cucumber/cucumber), allowing us to define our tests in plain-English .feature files.

Example .feature file:

Feature: Measurement Tools
  As a radiologist, I need to draw measurements to assess a study.

  Scenario: Drawing a new Length measurement
    Given a study is loaded in the viewport
    When I select the "Length" tool from the toolbar
    And I draw a line on the viewport
    Then I should see the measurement in the measurement panel

2. The Layered Testing Architecture

To implement these BDD scenarios, we will expand our current POM structure into a more granular, layered architecture: Steps -> Actions -> Page.

This separation makes our code incredibly reusable and easy to debug.

  • Layer 1: Pages (Page Objects)

    • Purpose: This is our current playwright/pages directory. Its responsibility is to define element locators and atomic interactions on a specific page or component.
    • Example: lengthToolButton.click(), measurementPanel.getRowCount().
    • Rule: Page Objects should never contain business logic or workflows. They only know how to find and interact with elements.
  • Layer 2: Actions (Workflow Layer)

    • Purpose: This is a new layer that consumes the "Page" objects. Actions represent reusable business workflows or tasks that a user can perform. They chain together multiple atomic "Page" interactions.
    • Example: An actions/measurement.ts file might have a function:
      async function drawLengthMeasurement(page, from, to) {
        await toolbarPage.clickTool('Length');
        await viewportPage.drag(from, to);
      }
    • Rule: This layer handles the logic of a workflow, while the "Page" layer handles the interaction.
  • Layer 3: Steps (Step Definitions)

    • Purpose: This is the "glue" layer that connects the Gherkin .feature files to our code. Each "Step" (Given, When, Then) will call one or more "Actions".
    • Example: A steps/measurement.steps.ts file:
      When('I select the {string} tool from the toolbar', async (toolName) => {
        await toolbarActions.selectTool(toolName);
      });
      
      Then('I should see the measurement in the measurement panel', async () => {
        await measurementPanelActions.verifyMeasurementExists();
      });

This flow (Feature -> Steps -> Actions -> Page) ensures that when a UI element changes, we only have to update the "Page" layer. If a workflow changes, we only update the "Action" layer. This makes the entire system highly scalable and robust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment