The Ultimate Guide: Best Practices and Architecture in Robot Framework Automation
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:
- 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.
- 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.
- Use the Right Tools: Tools like Robot Framework (with Selenium or Playwright) provide the necessary abstractions to interact with the browser reliably.
- 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 managementIn 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 Dashboard2. 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 loginorperform_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:
- ID or Data-Attributes: The fastest and most immutable.
${BTN_SAVE} id=btn-save ${BTN_SAVE} css=[data-testid='btn-save'] - Name: An excellent alternative when dynamic IDs are used.
${INPUT_NAME} name=user_first_name - CSS Selector: Performant, readable, and perfect for nested elements.
${ACTIVE_MENU} css=.sidebar > .menu-item.active - 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:
- Look for Data Attributes: Does the element have attributes like
data-testid,data-cy, ordata-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. - 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 (likeid=input-1234). - Use the Name attribute: Very common in forms, the
nameattribute is a great and safe fallback for mapping inputs and dropdowns. - 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 (likemt-4,text-center,bg-blue-500) as they can change during the first redesign. - 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/loginGood 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}/loginSecurity 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=10s6. 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
Clickin the tests layer, no selector in thestepslayer) - Are there no
Sleepcommands? - 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 todev, a full Regression suite instagingnightly, and a critical Health Check suite inproductionpost-deployment. - Run tests in Headless mode: Servers don't have graphical interfaces. Ensure your browser setup (in the
shared/base.resourcefile) uses--headless. - Parallel Execution: As your test suite grows, consider using tools like
pabotto run tests in parallel and reduce pipeline time. - Save Artifacts: Always upload the
log.htmlandreport.htmlartifacts 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.