
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.
•