- Mastering Selenium Python: From Beginner to Advanced Automation Expert – Lesson 1: Introduction to Selenium and Setting Up with Python
- Mastering Selenium Python: From Beginner to Advanced Automation Expert – Lesson 2: Opening Different Browsers Using Selenium Python
- Mastering Selenium Python: From Beginner to Advanced Automation Expert – Lesson 8: Test Automation Framework Design
- Mastering Selenium Python: From Beginner to Advanced Automation Expert – Lesson 3: Locating Web Elements with Selenium
- Mastering Selenium Python: From Beginner to Advanced Automation Expert – Lesson 4: Mastering CSS Selector Techniques for Selenium
In this final lesson, we’ll explore how to design and implement a comprehensive test automation framework using Selenium with Python. We’ll cover essential design patterns, best practices, and advanced features that make your automation framework robust, maintainable, and scalable.
Page Object Model Implementation
The Page Object Model (POM) is a design pattern that creates an object repository for storing web elements. Here’s how to implement it effectively:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By class BasePage: """Base class to initialize the base page that will be called from all pages""" def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(self.driver, timeout=10) def find_element(self, by, value): """Find element with explicit wait""" return self.wait.until( EC.presence_of_element_located((by, value)) ) def find_clickable_element(self, by, value): """Find clickable element with explicit wait""" return self.wait.until( EC.element_to_be_clickable((by, value)) ) def send_keys_to_element(self, by, value, text): """Send keys to element with explicit wait""" element = self.find_element(by, value) element.clear() element.send_keys(text) class LoginPage(BasePage): """Login page action methods""" def __init__(self, driver): super().__init__(driver) self.username_input = (By.ID, "username") self.password_input = (By.ID, "password") self.login_button = (By.CSS_SELECTOR, "button[type='submit']") def login(self, username, password): """Perform login action""" self.send_keys_to_element(*self.username_input, username) self.send_keys_to_element(*self.password_input, password) self.find_clickable_element(*self.login_button).click()
Data-Driven Testing Framework
Implement data-driven testing using external data sources:
import json import csv import yaml from dataclasses import dataclass from typing import List, Dict @dataclass class TestData: """Data class for test data""" scenario: str input_data: Dict expected_result: Dict class DataProvider: """Data provider for test cases""" @staticmethod def load_json_data(file_path: str) -> List[TestData]: """Load test data from JSON file""" with open(file_path, 'r') as file: data = json.load(file) return [TestData(**item) for item in data] @staticmethod def load_csv_data(file_path: str) -> List[TestData]: """Load test data from CSV file""" test_data = [] with open(file_path, 'r') as file: reader = csv.DictReader(file) for row in reader: test_data.append(TestData( scenario=row['scenario'], input_data=json.loads(row['input_data']), expected_result=json.loads(row['expected_result']) )) return test_data @staticmethod def load_yaml_data(file_path: str) -> List[TestData]: """Load test data from YAML file""" with open(file_path, 'r') as file: data = yaml.safe_load(file) return [TestData(**item) for item in data]
Logging and Reporting System
Create a comprehensive logging and reporting system:
import logging from datetime import datetime import os from typing import Optional import allure class TestLogger: """Custom logger for test automation""" def __init__(self, log_file: Optional[str] = None): self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.INFO) if not log_file: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_file = f"test_run_{timestamp}.log" file_handler = logging.FileHandler(log_file) console_handler = logging.StreamHandler() formatter = logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) self.logger.addHandler(file_handler) self.logger.addHandler(console_handler) def log_step(self, step_description: str): """Log test step with Allure reporting""" self.logger.info(f"Step: {step_description}") allure.step(step_description) def log_verification(self, verification_point: str, result: bool): """Log verification point with Allure reporting""" status = "PASSED" if result else "FAILED" message = f"Verification: {verification_point} - {status}" if result: self.logger.info(message) allure.step(message) else: self.logger.error(message) allure.step(message, status=allure.status.FAILED)
Configuration Management
Manage test configuration effectively:
import configparser from pathlib import Path from typing import Dict, Any class TestConfig: """Test configuration manager""" def __init__(self, config_file: str = "config.ini"): self.config = configparser.ConfigParser() self.config_file = Path(config_file) if not self.config_file.exists(): self._create_default_config() self.config.read(config_file) def _create_default_config(self): """Create default configuration file""" self.config['Environment'] = { 'base_url': 'https://example.com', 'browser': 'chrome', 'implicit_wait': '10', 'screenshot_dir': 'screenshots' } self.config['TestData'] = { 'data_file': 'test_data.json', 'data_format': 'json' } with open(self.config_file, 'w') as configfile: self.config.write(configfile) def get_browser_config(self) -> Dict[str, Any]: """Get browser configuration""" return { 'browser': self.config.get('Environment', 'browser'), 'implicit_wait': self.config.getint('Environment', 'implicit_wait') } def get_test_data_config(self) -> Dict[str, str]: """Get test data configuration""" return { 'data_file': self.config.get('TestData', 'data_file'), 'data_format': self.config.get('TestData', 'data_format') }
Test Execution Management
Create a test execution manager to handle test runs:
from typing import List, Optional import pytest from concurrent.futures import ThreadPoolExecutor import time class TestExecutionManager: """Manager for test execution""" def __init__(self, parallel_tests: int = 1): self.parallel_tests = parallel_tests self.start_time = None self.end_time = None def run_tests(self, test_cases: List[str], markers: Optional[List[str]] = None): """Run test cases with optional markers""" self.start_time = time.time() pytest_args = ['-v'] if markers: pytest_args.extend(['-m', ' or '.join(markers)]) if self.parallel_tests > 1: pytest_args.extend(['-n', str(self.parallel_tests)]) pytest_args.extend(test_cases) result = pytest.main(pytest_args) self.end_time = time.time() return result def get_execution_time(self) -> float: """Get test execution time in seconds""" if self.start_time and self.end_time: return self.end_time - self.start_time return 0.0
Screenshot and Video Recording
Implement advanced test artifacts collection:
import base64 from PIL import Image from io import BytesIO import cv2 import numpy as np class TestArtifactCollector: """Collector for test artifacts (screenshots, videos)""" def __init__(self, driver, artifact_dir: str = "artifacts"): self.driver = driver self.artifact_dir = Path(artifact_dir) self.artifact_dir.mkdir(exist_ok=True) def take_screenshot(self, name: str) -> str: """Take screenshot and save as PNG""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = self.artifact_dir / f"{name}_{timestamp}.png" screenshot = self.driver.get_screenshot_as_base64() image_data = base64.b64decode(screenshot) image = Image.open(BytesIO(image_data)) image.save(filename) return str(filename) def start_video_recording(self): """Start screen recording""" self.video_writer = cv2.VideoWriter( filename=str(self.artifact_dir / "test_recording.avi"), fourcc=cv2.VideoWriter_fourcc(*'XVID'), fps=20.0, frameSize=(1920, 1080) ) def capture_frame(self): """Capture current frame for video recording""" screenshot = self.driver.get_screenshot_as_base64() image_data = base64.b64decode(screenshot) image = Image.open(BytesIO(image_data)) frame = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) self.video_writer.write(frame) def stop_video_recording(self): """Stop video recording""" self.video_writer.release()
Putting It All Together
Here’s how to integrate all components into a complete framework:
class TestFramework: """Main test automation framework class""" def __init__(self, config_file: str = "config.ini"): self.config = TestConfig(config_file) self.logger = TestLogger() self.data_provider = DataProvider() self.execution_manager = TestExecutionManager() def initialize_driver(self): """Initialize WebDriver with configuration""" browser_config = self.config.get_browser_config() driver = webdriver.Chrome() # Add your browser setup logic self.artifact_collector = TestArtifactCollector(driver) return driver def run_test_suite(self, test_suite: str, parallel: bool = False): """Run complete test suite""" self.logger.log_step(f"Starting test suite: {test_suite}") try: driver = self.initialize_driver() test_data = self.load_test_data() if parallel: self.execution_manager.parallel_tests = 3 result = self.execution_manager.run_tests( test_cases=[test_suite], markers=['smoke', 'regression'] ) execution_time = self.execution_manager.get_execution_time() self.logger.log_step( f"Test suite completed in {execution_time:.2f} seconds" ) return result except Exception as e: self.logger.logger.error(f"Test suite failed: {str(e)}") raise finally: driver.quit() def load_test_data(self): """Load test data based on configuration""" data_config = self.config.get_test_data_config() data_format = data_config['data_format'] data_file = data_config['data_file'] if data_format == 'json': return self.data_provider.load_json_data(data_file) elif data_format == 'csv': return self.data_provider.load_csv_data(data_file) elif data_format == 'yaml': return self.data_provider.load_yaml_data(data_file)
Example Usage:
def test_login_scenario(): """Example test case using the framework""" framework = TestFramework() driver = framework.initialize_driver() try: login_page = LoginPage(driver) framework.logger.log_step("Navigating to login page") driver.get("https://example.com/login") test_data = framework.load_test_data() for data in test_data: framework.logger.log_step( f"Testing scenario: {data.scenario}" ) framework.artifact_collector.start_video_recording() try: login_page.login( data.input_data['username'], data.input_data['password'] ) result = driver.current_url == data.expected_result['url'] framework.logger.log_verification( "Login verification", result ) if not result: framework.artifact_collector.take_screenshot( f"login_failure_{data.scenario}" ) finally: framework.artifact_collector.stop_video_recording() finally: driver.quit()
Conclusion
This comprehensive test automation framework provides a solid foundation for creating maintainable and scalable automated tests. It incorporates essential features such as:
- Page Object Model for better maintainability
- Data-driven testing capabilities
- Robust logging and reporting
- Configuration management
- Test execution control
- Advanced artifact collection
Practice Exercises:
- Extend the framework to support API testing integration
- Implement parallel test execution with different browsers
- Add custom reporting templates
- Create a CI/CD pipeline integration module
- Implement cross-browser test execution management