Back to blog

The Ultimate Guide: Best Practices and Architecture in Robot Framework Automation

Automation, QA
#Robot Framework#Selenium#Best Practices#E2E#POM

End-to-End (E2E) test automation is a fundamental piece in ensuring continuous software quality. However, creating test scripts is only part of the challenge; building a robust, readable, and highly maintainable framework is what separates a mature project from a technical nightmare.

In this article, we will deeply explore architectural patterns and best practices to level up your automation using Robot Framework with SeleniumLibrary.


Understanding End-to-End (E2E) Testing

Before diving into the architecture, it's crucial to understand what End-to-End testing actually means.

End-to-End (E2E) testing is a methodology used to test whether the flow of an application is performing as designed from start to finish. The purpose of E2E testing is to identify system dependencies and to ensure that the data integrity is maintained between various system components. It simulates real user scenarios, essentially validating the software from the user's perspective, including interactions with the database, network, API, and UI.

Types of E2E Tests

E2E tests can generally be categorized based on their scope and focus:

  • Horizontal E2E: This is the most common form. It verifies a single user flow across multiple applications or screens. For example: A user logging in, adding an item to the cart, checking out, and verifying the order confirmation email.
  • Vertical E2E: Focuses on testing all the layers of a single application architecture from top to bottom (UI -> API -> Database) in isolation. It ensures that data flows correctly through the technology stack.
  • Regression E2E: Executed to ensure that new code changes haven't broken existing, previously working E2E business flows.
  • Smoke E2E: A quick subset of critical E2E tests run to verify that the core functionalities of the system are up and running before executing deeper tests.

How to do E2E Testing properly

Implementing E2E tests requires careful planning to avoid them becoming slow and fragile:

  1. Identify Critical User Journeys: You shouldn't E2E test everything. Focus on the core flows that generate value for the business (e.g., checkout process, user registration). Leave edge cases and negative scenarios for unit or integration tests.
  2. Setup and Teardown Data: E2E tests must be strictly independent. Create the necessary test data before the test starts and clean it up afterward. Never rely on existing state.
  3. Use the Right Tools: Tools like Robot Framework (with Selenium or Playwright) provide the necessary abstractions to interact with the browser reliably.
  4. Build a Robust Architecture: This is where the Page Object Model (POM) and clean architecture come in, which is the main focus of the rest of this guide.

1. Architecture: Page Object Model (POM)

The Page Object Model (POM) is an essential design pattern that reduces code duplication and drastically simplifies maintenance. If a page's interface changes, you only need to update the selectors in one place.

To better visualize this, we propose the following directory architecture to structure your automation project:

my-automation-project/
├── resources/
   ├── pages/            # Only selectors and element mapping (IDs, XPath, CSS)
      ├── login_page.resource
      └── dashboard_page.resource
   ├── steps/            # Interaction logic, business flows, and Selenium calls
      ├── login_steps.resource
      └── dashboard_steps.resource
   └── shared/           # Global keywords, Setups, Teardowns, and utilities
       └── base.resource
├── tests/                # Actual scenario files executed by Robot Framework
   ├── login_tests.robot
   └── dashboard_tests.robot
├── data/                 # Test data (CSVs, JSONs)
   └── users.json
├── config/               # Environment variables (URLs, configs)
   ├── dev.yaml
   └── prod.yaml
├── results/              # Logs, screenshots, and reports (auto-generated)
└── requirements.txt      # Dependency management

In our project, we apply a strict separation focused on the three main layers of this structure:

📂 Elements Layer (resources/pages/)

This layer serves exclusively as a repository for selectors (IDs, CSS, XPaths). It must not contain any logic or action calls.

Example (login_page.resource):

*** Variables ***
${LOGIN_INPUT_EMAIL}       id=email
${LOGIN_INPUT_PASSWORD}    id=password
${LOGIN_BUTTON_SUBMIT}     css=button[type='submit']
${LOGIN_ERROR_MESSAGE}     xpath=//div[contains(@class, 'alert-danger')]

📂 Action/Business Layer (resources/steps/)

This is where the magic happens. The steps import the pages layer and implement the actual interaction with the browser. The Keywords created here must be highly semantic.

Example (login_steps.resource):

*** Settings ***
Resource    ../pages/login_page.resource

*** Keywords ***
Fill Credentials And Submit
    [Arguments]    ${email}    ${password}
    Wait Until Element Is Visible    ${LOGIN_INPUT_EMAIL}    timeout=10s
    Input Text       ${LOGIN_INPUT_EMAIL}    ${email}
    Input Password   ${LOGIN_INPUT_PASSWORD}    ${password}
    Click Element    ${LOGIN_BUTTON_SUBMIT}

📂 Scenarios Layer (tests/)

Test files (.robot) should focus on "What" rather than "How". Reading a test case should be as clear as reading business documentation for technical and non-technical people alike (like QAs and POs).

Example (login_tests.robot):

*** Settings ***
Resource    ../resources/steps/login_steps.resource
Test Setup       Open Browser Safely
Test Teardown    Close Browser Safely

*** Test Cases ***
Scenario: Perform successful login
    [Tags]    smoke    login
    Access Login Page
    Fill Credentials And Submit    admin@test.com    securePassword123
    Validate Redirection To Dashboard

2. Naming Conventions

Consistency in naming is vital for a scalable project. Adopting a standard facilitates IDE auto-completion and immediate understanding of a file's or variable's purpose.

  • Keywords: Use Title Case (Capitalize the first letter of each word).
    • Perform Successful Login
    • perform successful login or perform_login
  • Element Variables: Use SCREAMING_SNAKE_CASE. Prefixing the variable with the screen name helps group the context.
    • ${HOME_TOP_MENU}
    • ${home_menu} or ${HomeMenu}
  • Files: Use snake_case.
    • registration_steps.resource
    • RegistrationSteps.resource

3. Selector Strategy and Priority

The main cause of flakiness in UI automation is poor selectors. Always follow this hierarchy of choice:

  1. ID or Data-Attributes: The fastest and most immutable.
    ${BTN_SAVE}    id=btn-save
    ${BTN_SAVE}    css=[data-testid='btn-save']
  2. Name: An excellent alternative when dynamic IDs are used.
    ${INPUT_NAME}    name=user_first_name
  3. CSS Selector: Performant, readable, and perfect for nested elements.
    ${ACTIVE_MENU}    css=.sidebar > .menu-item.active
  4. XPath: The absolute last resort. While powerful, it is slow and extremely fragile to the slightest page structure change. Use it anchored to texts only when strictly necessary.
    # Avoid absolute or overly long XPaths
    ${BTN_CANCEL}  xpath=//button[normalize-space()='Cancel']

Practical Step-by-Step for Mapping an Element:

When inspecting a button, input, or text in the browser (F12 > Inspect), follow this mental decision flowchart:

  1. Look for Data Attributes: Does the element have attributes like data-testid, data-cy, or data-qa? If so, use them immediately (e.g., css=[data-testid='submit-btn']). They are inserted by developers specifically for test automation and rarely change or break.
  2. Look for a unique ID: If there are no data attributes, check if the element has a clear, static id (e.g., id=email-input). Warning: Avoid dynamic IDs generated by frameworks (like id=input-1234).
  3. Use the Name attribute: Very common in forms, the name attribute is a great and safe fallback for mapping inputs and dropdowns.
  4. Build a robust CSS Selector: If there are no obvious unique attributes, create a CSS selector based on structural classes or a reliable parent-child relationship. Example: css=form.login-form > button.primary. Avoid using purely visual styling utility classes (like mt-4, text-center, bg-blue-500) as they can change during the first redesign.
  5. Resort to XPath (with caution): Only use XPath if the element has no useful classes or if you need to map it based on its inner text (very common for generic buttons). Always prefer relative, short XPaths: xpath=//button[contains(text(), 'Submit')].

4. Environment and Data Management

Never hardcode your scripts. A professional automation suite must be able to run across different environments (Local, Staging, Production) just by changing variables.

Bad Practice (Hardcoded):

Go To    https://dev-myapp.com/login

Good Practice (Variable-driven): In the execution command, we pass the environment variable: robot -v BASE_URL:https://dev-myapp.com tests/

*** Variables ***
${BASE_URL}    https://localhost:3000   # Default value

*** Keywords ***
Access System
    Go To    ${BASE_URL}/login

Security Warning: Never version passwords in your source code. Use password vaults or native secret variables from your CI/CD pipeline (e.g., GitHub Secrets).


5. Eliminate Hard Sleeps

Using Sleep is the biggest enemy of fast automation. If you set Sleep 5s and the screen loads in 1 second, you wasted 4 seconds. If it takes 6 seconds, the test fails.

Instead of Sleep, use smart waits (Explicit Waits):

#  INCORRECT: Blind wait
Click Element    ${BTN_SAVE}
Sleep    5s
Element Should Be Visible    ${SUCCESS_MESSAGE}

#  CORRECT: Test proceeds as soon as the element appears (with a safety timeout)
Click Element    ${BTN_SAVE}
Wait Until Element Is Visible    ${SUCCESS_MESSAGE}    timeout=10s

6. Quality Checklist (For your Pull Request)

Before opening a Pull Request to the main branch, ensure your code meets the framework's requirements:

  • Does the test run successfully locally (at least 3 consecutive times without flakiness)?
  • Was the POM architecture respected? (No Click in the tests layer, no selector in the steps layer)
  • Are there no Sleep commands?
  • Are the used selectors resilient (ID or Data-TestId)?
  • Is the environment being cleaned up after execution (Teardown restoring data or closing popups)?

7. CI/CD Integration

A robust automation framework is only truly valuable when it runs continuously. Integrating your E2E tests into a CI/CD pipeline (like GitHub Actions, GitLab CI, or Jenkins) ensures that no code reaches production without being validated.

Here is a practical example of how to configure a GitHub Actions workflow to run your Robot Framework tests automatically on every Pull Request:

name: E2E Tests
on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run Robot Framework Tests
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
        run: robot -d results tests/

      - name: Upload Test Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: robot-results
          path: results/

Key CI/CD Practices:

  • Environment Separation: Run different suites based on the target environment. For example, trigger a fast "Smoke Test" suite (robot -i smoke tests/) on every PR to dev, a full Regression suite in staging nightly, and a critical Health Check suite in production post-deployment.
  • Run tests in Headless mode: Servers don't have graphical interfaces. Ensure your browser setup (in the shared/base.resource file) uses --headless.
  • Parallel Execution: As your test suite grows, consider using tools like pabot to run tests in parallel and reduce pipeline time.
  • Save Artifacts: Always upload the log.html and report.html artifacts so you can debug failures easily.
  • Fail Fast: Configure your pipeline to stop immediately if critical setup tests (like authentication) fail, saving compute resources and providing faster feedback.

Following these guidelines will not only ensure your tests run better but will also make the automation a valuable and long-lasting asset for your development team.