DEV Community

Pradip Baskota
Pradip Baskota

Posted on

Introducing My Python Selenium Framework: A Solid Foundation for UI Testing

Let's be honest: UI test automation can feel like herding cats. You write a test, it passes. You run it again, it fails. You run it a third time, and it passes—just to keep you guessing. If you've ever wanted to throw your laptop out the window after a flaky test run, you're not alone. (Don't worry, your secret's safe with me.)

After years of wrestling with this chaos, I decided to build something that would bring order to the madness—a UI test automation framework that's not just reliable, but actually fun to use. (Yes, really!) Today, I'm excited to introduce this open-source framework, built with Python, Selenium, and Pytest. It's like a Swiss Army knife for UI testing, minus the risk of poking yourself.

This post kicks off a series where I'll dissect every part of this framework, sharing the design choices, the code, and the lessons learned (sometimes the hard way). My goal? To help you, dear reader, and to level up my own skills along the way.

You can view and clone the complete project on GitHub here: View the Framework on GitHub

The Philosophy: Stability, Maintainability, and Readability

This framework is built on three core principles:

  1. Stability Over Speed: A fast but flaky test is useless. This framework prioritizes stability above all else by embedding best practices like explicit waits and robust error handling into its very core.
  2. Maintainability is a Feature: Test code is production code. It must be clean, modular, and easy to refactor. A test that's hard to maintain will eventually be abandoned.
  3. Readability is Non-Negotiable: A good test should read like a manual test case. A non-technical stakeholder should be able to look at a test script and understand what it's trying to achieve.

An Architectural Tour

To achieve these goals, the framework is organized into a clean, layered architecture. This separation of concerns is crucial for maintainability.

ui-test-automation/
├── base/         # Core components: Selenium wrappers, WebDriver factory
├── pages/        # Page Object Model: Locators and actions for each page
├── tests/        # The actual Pytest test scripts
├── testdata/     # Configuration files and test data
└── utils/        # Helper utilities: logging, reporting, etc.
Enter fullscreen mode Exit fullscreen mode
  • base/: The engine of the framework. It handles the low-level browser interactions.
  • pages/: Implements the Page Object Model (POM), abstracting page details away from the tests.
  • tests/: Contains the high-level test logic. These files describe what to test, not how to test it.
  • testdata/: Manages configuration, separating sensitive data and environment-specific settings from the code.
  • utils/: A collection of tools that support the framework, like a custom logger and assertion tracker.

The Three Pillars of the Framework

Three key patterns form the foundation of this project:

1. The Page Object Model (POM)

The Page Object Model is a design pattern that organizes your test code by giving each page (or component) its own class. This class contains all the locators and actions for that page, making your tests easier to read, maintain, and scale.

# pages/demoqa/elements_page.py

class ElementsPage(DemoQABase):
    # --- Locators (strategy, value, description) ---
    HEADER_LINK = ("css", "div#app a[href='https://demoqa.com']", "Header Link")
    TEXT_BOX_MENU = ("xpath", "//span[contains(text(), 'Text Box')]", "Elements Card")

    def __init__(self, driver):
        super().__init__(driver)
        self.url = f'{self.base_url}elements'

    def is_at(self):
        """Verifies we are on the correct page."""
        return self.is_element_present(self.TEXT_BOX_MENU)
Enter fullscreen mode Exit fullscreen mode

If a UI locator changes, you only need to update it in one place. Your future self will thank you.

2. The Selenium Wrapper

Instead of calling Selenium commands directly, our Page Objects inherit from a powerful SeleniumWrapper class. This wrapper enhances basic commands with built-in explicit waits, detailed logging, and smart error handling.

Consider the click() method. It doesn't just click; it waits for the element to be clickable, logs the action, and handles multiple types of exceptions gracefully.

# base/selenium_wrapper.py

def click(self, locator_full, timeout=None):
    """Clicks an element after ensuring it's clickable."""
    timeout = timeout if timeout is not None else self.timeout
    self.log.debug(f"Attempting to click element: {locator_full[2]}")

    element = self.explicitly_wait(locator_full, timeout) # Wait first!
    if not element:
        self.log.error(f"Cannot proceed with click - element not available: {locator_full[2]}")
        return
    try:
        element.click()
        self.log.info(f"Successfully clicked element: {locator_full[2]}")
    except StaleElementReferenceException:
        self.log.error(f"Element became stale before clicking: {locator_full[2]}")
    # ... other exceptions
Enter fullscreen mode Exit fullscreen mode

3. Smart Configuration Management

A framework needs to run on a developer's machine and in a CI/CD pipeline without code changes. Our configuration system handles this by using a layered approach, merging a base config, a local config, and a secrets file.

A simple boolean flag, TEST_RUN_ON_SERVER, determines whether to load settings from local files or from a secure cloud service like AWS Parameter Store, making it perfect for any environment.

Bringing It All Together: A Sample Test

So what does a test look like? Thanks to the architecture, it's clean and readable.

# tests/test_demoqa/test_elements_page.py

@allure.description("validates whether Text Box Page is Working")
@pytest.mark.smoke
def test_text_box_input(self, navigate_to_elements):
    self.log.info("::: test_text_box_input")
    elements_page = navigate_to_elements # Fixture handles navigation

    with allure.step('Navigating to the TextBox Page'):
        text_box_page = elements_page.open_text_box()
        self.status_tracker.assert_true(text_box_page.is_at(), "Navigation to Text Box Page Check")

    with allure.step('Entering Details to the textbox and Submitting'):
        text_box_page.submit_text(
            fullname="Testing Full Name",
            email="[email protected]",
            current_address="Testing Current Address",
            permanent_address="Testing Permanent Address"
        )

    with allure.step('Verifying the inputs are correctly displayed'):
        self.status_tracker.assert_true(
            text_box_page.check_output(...), "Output Verification"
        )
Enter fullscreen mode Exit fullscreen mode

Notice how the test describes the user's journey. It uses plain English methods from the Page Objects and delegates all the messy implementation details to the framework.

What's Next?

This framework is a solid starting point, but it's just the beginning. It's a living project that I plan to improve continuously.

In my next post, I'll do a deep dive into the Page Object Model, exploring why it's such a critical pattern for writing maintainable tests and sharing more advanced techniques.

Check out the GitHub repository, clone it, run it, and see it for yourself. Leave a comment below with your thoughts, questions, or favorite automation memes. Let's build better automation, together!

Top comments (0)