随着产品功能的不断增长以及测试用例的日益复杂,自动化测试框架逐渐暴露出一系列挑战,列如开发速度难以跟上产品迭代、集成测试和回归测试负担加重,无奈之下为了快速的开发自动化测试不断的复制粘贴导致更难以维护。
为了让测试框架易于维护、易于扩展,并且能够轻松快速的适配新测试用例,同时又能确保测试结果的稳定性,减少脆弱测试,我们需要引入更好的架构设计和合理的设计模式。
测试框架难以扩展的常见原因
-
缺乏模块化和可维护性
缺乏模块组件的单体框架可能变得僵化且难以更新。随着应用程序添加更多功能,测试套件不断增长,缺乏模块化,即使是小的更改也可能破坏大量测试。
-
缺乏设计模式
糟糕的框架设计,例如不使用或错误的使用页面对象模型(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 = driverself.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 osclass 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 threadingclass 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() # 只创建一次 WebDriverreturn cls._instancedef 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 contextmanagerclass 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._instancedef _initialize(self, dbname, user, password, host, port):"""初始化数据库连接"""self.dbname = dbnameself.user = userself.password = passwordself.host = hostself.port = portself.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 = Nonereturn self.conndef 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}")raisedef 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 wrapsdef 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 resultreturn wrapper# 示例:测试类
class BaseTest:@time_loggerdef run_test(self):"""模拟一个执行时间较长的测试"""time.sleep(2)print("Test executed")# 运行测试
test = BaseTest()
test.run_test()
使用装饰器模式进行异常处理
在测试中某些测试可能会比较不稳定,我们可以使用装饰器模式自动捕获异常进行异常处理,比如重试、截图、记录日志等
import time
import random
from functools import wrapsdef retry_on_failure(retries=3, delay=1):"""装饰器:如果测试失败,则自动重试"""def decorator(func):@wraps(func)def wrapper(*args, **kwargs):attempts = 0while attempts < retries:try:return func(*args, **kwargs)except Exception as e:attempts += 1print(f"Test {func.__name__} failed: {e}. Retrying {attempts}/{retries}...")time.sleep(delay)raise Exception(f"Test {func.__name__} failed after {retries} retries")return wrapperreturn 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()