Thursday, 2 January, 2025
HomeProgrammingPythonMastering Selenium Python: From Beginner to Advanced Automation Expert – Lesson 8:...

Mastering Selenium Python: From Beginner to Advanced Automation Expert – Lesson 8: Test Automation Framework Design

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:

  1. Page Object Model for better maintainability
  2. Data-driven testing capabilities
  3. Robust logging and reporting
  4. Configuration management
  5. Test execution control
  6. Advanced artifact collection

Practice Exercises:

  1. Extend the framework to support API testing integration
  2. Implement parallel test execution with different browsers
  3. Add custom reporting templates
  4. Create a CI/CD pipeline integration module
  5. Implement cross-browser test execution management
Series Navigation<< Mastering Selenium Python: From Beginner to Advanced Automation Expert – Lesson 7: Advanced Browser Controls
Related articles

Most Popular