Back to Blog
Technology
May 10, 2025
4 min read
729 words

Building a Production-Ready Selenium Python Framework

Create a scalable Selenium testing framework in Python. Covers pytest integration, fixtures, reporting, and best practices for enterprise testing.

Building a Production-Ready Selenium Python Framework

Why Build a Custom Framework?

After building test frameworks for dozens of companies, I have learned that the best frameworks are tailored to specific needs while following proven patterns. This guide shows you how to build a production-ready Selenium Python framework from the ground up.

Project Structure

selenium-framework/
├── config/
│   ├── __init__.py
│   ├── config.py
│   └── environments.yaml
├── pages/
│   ├── __init__.py
│   ├── base_page.py
│   ├── login_page.py
│   └── dashboard_page.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_login.py
│   └── test_dashboard.py
├── utilities/
│   ├── __init__.py
│   ├── driver_factory.py
│   ├── logger.py
│   └── helpers.py
├── reports/
├── screenshots/
├── requirements.txt
├── pytest.ini
└── README.md

Core Dependencies

# requirements.txt
selenium==4.16.0
pytest==7.4.3
pytest-html==4.1.1
pytest-xdist==3.5.0
webdriver-manager==4.0.1
python-dotenv==1.0.0
allure-pytest==2.13.2
PyYAML==6.0.1

Configuration Management

# config/config.py
import os
from dotenv import load_dotenv
import yaml

load_dotenv()

class Config:
    BASE_URL = os.getenv('BASE_URL', 'https://example.com')
    BROWSER = os.getenv('BROWSER', 'chrome')
    HEADLESS = os.getenv('HEADLESS', 'false').lower() == 'true'
    IMPLICIT_WAIT = int(os.getenv('IMPLICIT_WAIT', 10))
    EXPLICIT_WAIT = int(os.getenv('EXPLICIT_WAIT', 20))
    
    @classmethod
    def load_environment(cls, env_name):
        with open('config/environments.yaml', 'r') as file:
            envs = yaml.safe_load(file)
            return envs.get(env_name, {})
# config/environments.yaml
staging:
  base_url: https://staging.example.com
  timeout: 30

production:
  base_url: https://www.example.com
  timeout: 20

qa:
  base_url: https://qa.example.com
  timeout: 30

Driver Factory

# utilities/driver_factory.py
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.firefox.service import Service as FirefoxService
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
from config.config import Config

class DriverFactory:
    @staticmethod
    def get_driver(browser=None, headless=None):
        browser = browser or Config.BROWSER
        headless = headless if headless is not None else Config.HEADLESS
        
        if browser.lower() == 'chrome':
            options = webdriver.ChromeOptions()
            if headless:
                options.add_argument('--headless=new')
            options.add_argument('--no-sandbox')
            options.add_argument('--disable-dev-shm-usage')
            options.add_argument('--window-size=1920,1080')
            
            service = ChromeService(ChromeDriverManager().install())
            driver = webdriver.Chrome(service=service, options=options)
            
        elif browser.lower() == 'firefox':
            options = webdriver.FirefoxOptions()
            if headless:
                options.add_argument('--headless')
            
            service = FirefoxService(GeckoDriverManager().install())
            driver = webdriver.Firefox(service=service, options=options)
            
        else:
            raise ValueError(f'Unsupported browser: {browser}')
        
        driver.implicitly_wait(Config.IMPLICIT_WAIT)
        driver.maximize_window()
        return driver

Base Page Class

# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import TimeoutException
from config.config import Config
import logging

logger = logging.getLogger(__name__)

class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, Config.EXPLICIT_WAIT)
    
    def find_element(self, locator):
        try:
            element = self.wait.until(EC.presence_of_element_located(locator))
            return element
        except TimeoutException:
            logger.error(f'Element not found: {locator}')
            raise
    
    def click(self, locator):
        element = self.wait.until(EC.element_to_be_clickable(locator))
        element.click()
        logger.info(f'Clicked element: {locator}')
    
    def type_text(self, locator, text):
        element = self.find_element(locator)
        element.clear()
        element.send_keys(text)
        logger.info(f'Typed "{text}" into: {locator}')
    
    def get_text(self, locator):
        element = self.find_element(locator)
        return element.text
    
    def is_displayed(self, locator, timeout=5):
        try:
            wait = WebDriverWait(self.driver, timeout)
            wait.until(EC.visibility_of_element_located(locator))
            return True
        except TimeoutException:
            return False
    
    def hover(self, locator):
        element = self.find_element(locator)
        ActionChains(self.driver).move_to_element(element).perform()
    
    def take_screenshot(self, name):
        path = f'screenshots/{name}.png'
        self.driver.save_screenshot(path)
        logger.info(f'Screenshot saved: {path}')
        return path

Page Object Example

# pages/login_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage

class LoginPage(BasePage):
    # Locators
    USERNAME_INPUT = (By.ID, 'username')
    PASSWORD_INPUT = (By.ID, 'password')
    LOGIN_BUTTON = (By.CSS_SELECTOR, 'button[type="submit"]')
    ERROR_MESSAGE = (By.CLASS_NAME, 'error-message')
    
    def __init__(self, driver):
        super().__init__(driver)
        self.url = '/login'
    
    def navigate(self):
        from config.config import Config
        self.driver.get(f'{Config.BASE_URL}{self.url}')
        return self
    
    def login(self, username, password):
        self.type_text(self.USERNAME_INPUT, username)
        self.type_text(self.PASSWORD_INPUT, password)
        self.click(self.LOGIN_BUTTON)
        return self
    
    def get_error_message(self):
        return self.get_text(self.ERROR_MESSAGE)
    
    def is_error_displayed(self):
        return self.is_displayed(self.ERROR_MESSAGE)

Pytest Fixtures

# tests/conftest.py
import pytest
from utilities.driver_factory import DriverFactory
from pages.login_page import LoginPage

@pytest.fixture(scope='function')
def driver():
    driver = DriverFactory.get_driver()
    yield driver
    driver.quit()

@pytest.fixture(scope='function')
def login_page(driver):
    return LoginPage(driver)

@pytest.fixture(scope='session')
def test_data():
    return {
        'valid_user': {'username': 'testuser', 'password': 'password123'},
        'invalid_user': {'username': 'wrong', 'password': 'wrong'}
    }

# Screenshot on failure
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    if rep.when == 'call' and rep.failed:
        driver = item.funcargs.get('driver')
        if driver:
            driver.save_screenshot(f'screenshots/{item.name}_failure.png')

Test Examples

# tests/test_login.py
import pytest
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage

class TestLogin:
    @pytest.mark.smoke
    def test_successful_login(self, login_page, test_data):
        user = test_data['valid_user']
        login_page.navigate().login(user['username'], user['password'])
        
        dashboard = DashboardPage(login_page.driver)
        assert dashboard.is_displayed(dashboard.WELCOME_MESSAGE)
    
    @pytest.mark.regression
    def test_invalid_credentials(self, login_page, test_data):
        user = test_data['invalid_user']
        login_page.navigate().login(user['username'], user['password'])
        
        assert login_page.is_error_displayed()
        assert 'Invalid credentials' in login_page.get_error_message()
    
    @pytest.mark.regression
    @pytest.mark.parametrize('username,password,error', [
        ('', 'password', 'Username is required'),
        ('user', '', 'Password is required'),
        ('', '', 'Username is required'),
    ])
    def test_validation_errors(self, login_page, username, password, error):
        login_page.navigate().login(username, password)
        
        assert login_page.is_error_displayed()
        assert error in login_page.get_error_message()

pytest.ini Configuration

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    smoke: Quick sanity tests
    regression: Full regression suite
    slow: Long-running tests
addopts = -v --html=reports/report.html --self-contained-html
filterwarnings = ignore::DeprecationWarning

Running Tests

# Run all tests
pytest

# Run smoke tests
pytest -m smoke

# Parallel execution
pytest -n 4

# With Allure reporting
pytest --alluredir=reports/allure-results
allure serve reports/allure-results

Key Takeaways

  • Clean project structure enables maintainability
  • Configuration management handles multiple environments
  • Page Object Model provides abstraction
  • Pytest fixtures manage setup/teardown
  • Proper reporting aids debugging
Tags:TechnologyTutorialGuide
X

Written by XQA Team

Our team of experts delivers insights on technology, business, and design. We are dedicated to helping you build better products and scale your business.