当前位置: 首页 > news >正文

自动化测试中使用的设计模式

随着产品功能的不断增长以及测试用例的日益复杂,自动化测试框架逐渐暴露出一系列挑战,列如开发速度难以跟上产品迭代集成测试和回归测试负担加重,无奈之下为了快速的开发自动化测试不断的复制粘贴导致更难以维护。

为了让测试框架易于维护、易于扩展,并且能够轻松快速的适配新测试用例,同时又能确保测试结果的稳定性,减少脆弱测试,我们需要引入更好的架构设计和合理的设计模式。


测试框架难以扩展的常见原因

  • 缺乏模块化和可维护性

    缺乏模块组件的单体框架可能变得僵化且难以更新。随着应用程序添加更多功能,测试套件不断增长,缺乏模块化,即使是小的更改也可能破坏大量测试。

  • 缺乏设计模式

    糟糕的框架设计,例如不使用或错误的使用页面对象模型(POM)等设计模式,可能导致脆弱的测试用例。

  • 无法管理日益增长的测试数据和环境

    随着应用程序的增长,测试数据和环境配置的数量也不断增加。

  • 过度依赖硬编码元素

    页面元素、测试数据或环境配置的硬编码写死方式可能使得自动化测试适应程序演变时愈发困难。

为了解决这些问题,我们可以借鉴软件开发中的设计模式,使测试框架更加模块化、可扩展、稳定。本篇文章将介绍几种常见的自动化测试设计模式,并结合代码示例探讨其应用。


常见的自动化测试设计模式

Page Object Model(POM)

POM是广泛应用于Web/UI 自动化测试的最佳实践,严格来说并不是设计模式之一。但它借鉴了很多设计模式的思想 ,它的核心是封装 UI 交互逻辑,将测试代码与页面元素分离,提高可维护性

为什么使用POM

1️⃣ 可维护性(Maintainability)

在 Web 应用的开发过程中,如果测试代码直接去操作页面上的对象,一旦 UI 发生变动测试用例就可能全部失效,就会造成散弹式的修改,维护成本很高。

POM对被测试页面进行抽象和封装,将页面元素和操作方法交互逻辑集中在一个类中。当 UI 发生变化时,只需要修改对应的 Page 类,从而提升测试套件的可维护性。

2️⃣ 代码复用性(Code Reusability)

POM通过封装页面上的操作可以使不同的测试脚本可以复用相同的交互逻辑,减少代码重复。

例如,多个测试用例可能都需要登录功能,如果每个测试都手动输入用户名和密码,代码会大量冗余。但使用 POM,可以封装 login() 方法,在不同的测试中直接调用,提高代码复用性。

示例:

class LoginPage:
    def __init__(self, driver):
        self.driver = driver
        self.username_field = driver.find_element(By.ID, "username")
        self.password_field = driver.find_element(By.ID, "password")
        self.login_button = driver.find_element(By.ID, "login")

    def login(self, username, password):
        self.username_field.send_keys(username)
        self.password_field.send_keys(password)
        self.login_button.click()

3️⃣ 让测试逻辑与 UI 结构解耦(Decoupling Test Logic from UI)

使用了POM设计后,就间接的为测试框架引入了分层设计的概念,上层的测试逻辑和底层的页面解耦。这种解耦方式使得测试层的代码可以专注于测试本身,而不依赖具体的操作或元素。

❌ 传统方式(测试代码直接操作 UI)

def test_login(driver):
    driver.find_element(By.ID, "username").send_keys("admin")
    driver.find_element(By.ID, "password").send_keys("password")
    driver.find_element(By.ID, "login").click()
    assert "Dashboard" in driver.title

如果有多个不同的登录测试用例(如不同用户名、密码错误、验证码验证等)。一旦页面中的某个元素发生改变,或者登录页面多了一个验证框时、所有涉及登录的测试用例都需要修改。


Factory Pattern - 工厂模式

工厂模式(Factory Pattern)的主要目的是将对象的创建与使用分离,让调用者无需关心对象的具体创建过程,而是通过工厂方法获得所需的对象。 比如我们进入一个披萨店,披萨店就类似一个制作披萨的工厂,我们只需要告诉店员你想要哪种披萨(芝士披萨、意大利辣肠披萨等),而不需要关心披萨的制作过程,厨房就会根据订单制作披萨

使用 Factory Pattern 管理 WebDriver

在UI自动化测试中一般需要针对不同的浏览器(Chrome、Firefox、IE)运行测试,工厂模式可以提供一个统一的方式来动态创建 WebDriver 实例。下面演示一下如何通过工厂模式去创建不同的浏览器需要WebDriver

from selenium import webdriver
import os

class WebDriverFactory:
    def __init__(self, browser="chrome"):
        """
        Initializes WebDriverFactory with browser type and base URL.

        Args:
            browser (str): Browser type (chrome, firefox, iexplorer).
        """
        self.browser = browser.lower()

    def get_webdriver_instance(self):
        """
        Creates and returns a WebDriver instance based on the browser type.

        Returns:
            WebDriver: The initialized WebDriver instance.
        """
        drivers = {
            "chrome": lambda: webdriver.Chrome(),
            "firefox": lambda: webdriver.Firefox(),
            "iexplorer": lambda: webdriver.Ie()
        }

        driver = drivers.get(self.browser, drivers["chrome"])()
        return driver

# 示例:使用工厂模式创建 WebDriver 实例
if __name__ == "__main__":
    factory = WebDriverFactory(browser="firefox")
    driver = factory.get_webdriver_instance()
  • __init__ 方法接收一个 browser 参数,用于指定要创建的浏览器类型。
  • drivers.get(self.browser, drivers["chrome"]) 会根据初始化时传入的 browser 选择相应的 WebDriver 实例。
  • 如果 self.browser 不是 "chrome"、"firefox" 或 "iexplorer",则默认使用 Chrome。

Singleton Pattern - 单例模式

它确保在整个应用程序中只有一个特定对象的实例。这在测试自动化中非常有用,当你想在不同的测试用例之间共享资源,比如数据库连接或WebDriver实例时。

1️⃣ WebDriver 的单例模式

在 UI 自动化测试 中,如果我们希望所有测试都共享一个WebDriver实例,而不是每次运行测试都重新打开一个浏览器窗口。这不仅能提高执行效率,还能减少 WebDriver 进程的重复创建和销毁。

from selenium import webdriver
import threading

class WebDriverSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        with cls._lock:
            if cls._instance is None:
                cls._instance = super(WebDriverSingleton, cls).__new__(cls)
                cls._instance.driver = webdriver.Chrome()  # 只创建一次 WebDriver
        return cls._instance

    def get_driver(self):
        return self.driver

# 示例
if __name__ == "__main__":
    driver1 = WebDriverSingleton().get_driver()
    driver2 = WebDriverSingleton().get_driver()
    
    print(driver1 is driver2)  # True,确保只有一个实例

2️⃣ 数据库连接的单例模式

在自动化测试中,数据库访问是常见需求。如果每次查询都创建新的数据库连接,可能会增加数据库负载,影响测试环境的稳定性。使用单例模式可以确保整个测试过程中复用同一个数据库连接,从而提高性能并减少资源消耗。

import psycopg2
import threading
from contextlib import contextmanager

class PostgresDB:
    _instance = None  # 单例实例
    _lock = threading.Lock()  # 线程安全锁

    def __new__(cls, dbname, user, password, host='localhost', port='5432'):
        with cls._lock:  # 确保多线程下只有一个实例
            if cls._instance is None:
                cls._instance = super(PostgresDB, cls).__new__(cls)
                cls._instance._initialize(dbname, user, password, host, port)
        return cls._instance

    def _initialize(self, dbname, user, password, host, port):
        """初始化数据库连接"""
        self.dbname = dbname
        self.user = user
        self.password = password
        self.host = host
        self.port = port
        self.conn = None  # 持久化数据库连接

    def get_connection(self):
        """获取数据库连接,确保连接复用"""
        if self.conn is None or self.conn.closed:
            try:
                self.conn = psycopg2.connect(
                    dbname=self.dbname,
                    user=self.user,
                    password=self.password,
                    host=self.host,
                    port=self.port
                )
            except psycopg2.DatabaseError as e:
                print(f"数据库连接失败: {e}")
                self.conn = None
        return self.conn

    def execute_query(self, sql, params=None):
        """执行数据库查询"""
        conn = self.get_connection()
        if conn is None:
            raise RuntimeError("数据库连接不可用")

        with conn.cursor() as cur:
            try:
                cur.execute(sql, params)
                if cur.description:  # 只查询时返回数据
                    return cur.fetchall()
                conn.commit()  # 插入/更新操作提交
            except psycopg2.DatabaseError as e:
                conn.rollback()
                print(f"SQL 执行错误: {e}")
                raise

    def close_connection(self):
        """在测试结束后关闭数据库连接"""
        if self.conn is not None:
            self.conn.close()
            self.conn = None  # 清空连接实例

# 使用示例
if __name__ == "__main__":
    db = PostgresDB(dbname="testdb", user="admin", password="password")

    # 执行查询
    result = db.execute_query("SELECT * FROM users WHERE id = %s", (1,))
    print(result)

    db.close_connection()  # 关闭数据库连接

Decorator Pattern - 装饰器模式

装饰器模式是一种结构型设计模式,它允许在不修改现有对象的情况下动态地添加新功能。

说到装饰器模式,很容易想到Python中的Decorator装饰器,两者在在概念上相似但不完全相同。装饰器模式是面向对象编程中的一种设计模式,它的核心思想在不修改原有代码的情况下扩展某个类或函数的功能

Python中的装饰器是一种语法糖,用于动态修改函数的行为,而不需要修改函数的定义。Python中的装饰器可以理解Python为开发者提供的一种实现装饰器模式的方式

在 测试框架中,装饰器模式可以用于:

  • 统计测试执行时间(分析测试性能,发现慢速测试)
  • 异常处理(捕获测试失败信息,避免测试崩溃)

使用装饰器模式记录测试执行时间

在自动化测试中如果需要对测试步骤或函数统计执行时间,使用装饰器就可以很方便优雅的实现

import time
from functools import wraps

def time_logger(func):
    """装饰器:记录测试执行时间"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Test {func.__name__} executed in {end_time - start_time:.4f} seconds")
        return result
    return wrapper

# 示例:测试类
class BaseTest:
    @time_logger
    def run_test(self):
        """模拟一个执行时间较长的测试"""
        time.sleep(2)
        print("Test executed")

# 运行测试
test = BaseTest()
test.run_test()

使用装饰器模式进行异常处理

在测试中某些测试可能会比较不稳定,我们可以使用装饰器模式自动捕获异常进行异常处理,比如重试、截图、记录日志等

import time
import random
from functools import wraps

def retry_on_failure(retries=3, delay=1):
    """装饰器:如果测试失败,则自动重试"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    print(f"Test {func.__name__} failed: {e}. Retrying {attempts}/{retries}...")
                    time.sleep(delay)
            raise Exception(f"Test {func.__name__} failed after {retries} retries")
        return wrapper
    return decorator

# 示例:测试类
class NetworkTest:
    @retry_on_failure(retries=3, delay=2)
    def unstable_test(self):
        """模拟不稳定的测试(30% 失败概率)"""
        if random.random() < 0.3:
            raise Exception("Random network failure")
        print("Test executed successfully")

# 运行测试
test = NetworkTest()
test.unstable_test()

相关文章:

  • 设计模式之单例模式(Singleton Pattern)
  • Kafka--常见问题
  • python-selenium 爬虫 由易到难
  • Linux实时内核 - 启用 RCU(Read-Copy Update)机制的性能测试功能
  • Excel中如何自动计算累计销量,当具体销量为空时公式自动不计算
  • 2025-03-22 学习记录--C/C++-PTA 习题4-11 兔子繁衍问题
  • 2025新版懒人精灵零基础安装调试+lua基础+UI设计交互+常用方法封装+项目实战+项目打包安装板块-视频教程(初学者必修课)
  • Linux中动静态库的创建与原理
  • Electron Forge【实战】桌面应用 —— AI聊天(上)
  • 遨游三防 | IP68热成像三防平板,助力电力智慧巡检
  • git推送代码相关学习——(一)
  • Flutter中常用命令
  • 区块链(Blockchain)—— 概念、架构与应用
  • 信奥赛CSP-J复赛集训(模拟算法专题)(27):P5016 [NOIP 2018 普及组] 龙虎斗
  • 基于AWS Endpoint Security(EPS)的混合云统一安全管理
  • java牛排烧烤技术
  • 标题word技巧 :匹配所有的 [数字],替换成上标
  • centos7安装单机zookeeper
  • arm linux下的读写信号量rw_semphore的实现
  • macOS 使用 enca 识别 文件编码类型(比 file 命令准确)
  • 被取消总统候选人资格,金文洙:将采取政治法律措施讨回公道
  • 美联储连续第三次维持利率不变,警示关税影响
  • 中方对原产印度进口氯氰菊酯实施反倾销措施,商务部回应
  • 马克思主义理论研究教学名师系列访谈|鲍金:给予学生一碗水、自己就要有一桶水
  • 繁荣活跃!“五一”假期全国重点零售和餐饮企业销售额同比增长6.3%
  • 特朗普宣布对进口电影征收100%关税