1. POM设计模式:自动化测试脚本的"活"灵魂
第一次接触Selenium自动化测试时,我像大多数新手一样,把元素定位和操作逻辑全部堆在一个脚本文件里。不到两周,这个2000行的.py文件就成了没人敢碰的"祖传代码"——每次页面改版都要花半天时间在茫茫xpath中寻找需要修改的那几行。直到遇到POM(Page Object Model)设计模式,才真正明白什么是可维护的自动化测试代码。
POM本质上是一种将页面对象与测试逻辑分离的设计模式。想象你正在组装宜家家具:POM就是把螺丝、木板这些零件(页面元素)分类放在不同的塑料袋(Page类)里,并附上组装说明书(操作方法)。当某个零件规格变更时,你只需要更新对应的袋子,而不必拆开整个家具。在电商网站的测试场景中,这意味着登录页的元素定位变化时,你只需修改LoginPage.py,而不影响购物车或商品详情页的测试逻辑。
2. POM核心架构解析
2.1 三层结构设计
典型的POM实现包含三个关键层:
基础层(Base)
相当于测试框架的"地基",包含:- WebDriver初始化配置(浏览器类型、隐式等待等)
- 通用工具方法(截图、日志、数据生成)
- 异常处理机制
# base_page.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver = driver self.timeout = 10 def click(self, locator): WebDriverWait(self.driver, self.timeout).until( EC.element_to_be_clickable(locator) ).click()页面对象层(Page Objects)
每个页面对应一个类,遵循两大原则:- 元素定位器与操作方法的封装
- 不包含断言逻辑(这是测试层的职责)
# login_page.py from .base_page import BasePage from selenium.webdriver.common.by import By class LoginPage(BasePage): USERNAME = (By.ID, 'username') PASSWORD = (By.NAME, 'pwd') SUBMIT_BTN = (By.XPATH, '//button[@type="submit"]') def enter_credentials(self, user, pwd): self.driver.find_element(*self.USERNAME).send_keys(user) self.driver.find_element(*self.PASSWORD).send_keys(pwd) def submit(self): self.click(self.SUBMIT_BTN)测试用例层(Test Cases)
专注于业务验证逻辑:- 组织页面对象完成测试流程
- 包含断言语句
- 处理测试数据
# test_login.py import pytest from pages.login_page import LoginPage @pytest.mark.regression def test_valid_login(browser): login_page = LoginPage(browser) login_page.load() login_page.enter_credentials("standard_user", "secret_sauce") login_page.submit() assert "inventory" in browser.current_url
2.2 元素定位策略优化
在大型项目中,元素定位器的维护成本往往最高。我总结出几个实用技巧:
定位器分级管理
将定位器按优先级排序:- 首选ID(最稳定)
- 次选CSS Selector(性能优于XPath)
- 慎用XPath(除非处理动态层级)
动态元素处理
对于AJAX加载的元素,使用显式等待而非硬性sleep:def wait_for_element(self, locator): return WebDriverWait(self.driver, self.timeout).until( EC.presence_of_element_located(locator) )定位器版本控制
当页面改版时,可以通过继承机制保留旧定位器:class NewLoginPage(LoginPage): # 重写发生变化的定位器 USERNAME = (By.CSS_SELECTOR, '.new-username-field') # 其他定位器保持继承
3. 高级POM实战技巧
3.1 页面组件化设计
当多个页面包含相同UI组件(如导航栏、侧边菜单)时,可以抽象出Component类:
# components/navbar.py class NavBar: CART_ICON = (By.CLASS_NAME, 'shopping_cart_link') def __init__(self, driver): self.driver = driver def go_to_cart(self): self.driver.find_element(*self.CART_ICON).click()在页面类中组合使用:
class ProductPage(BasePage): def __init__(self, driver): super().__init__(driver) self.nav = NavBar(driver) # 测试中使用 product_page.nav.go_to_cart()3.2 数据驱动测试集成
结合pytest的参数化功能实现数据驱动:
# conftest.py import pytest import json def load_testdata(file): with open(file) as f: return json.load(f) @pytest.fixture(params=load_testdata('login_data.json')) def login_data(request): return request.param # test_login.py def test_login_combinations(browser, login_data): login_page = LoginPage(browser) login_page.attempt_login(login_data['user'], login_data['pwd']) assert login_data['expected'] in browser.current_url对应的测试数据文件:
// login_data.json [ { "user": "locked_user", "pwd": "secret_sauce", "expected": "error" }, { "user": "standard_user", "pwd": "wrong_pwd", "expected": "error" } ]3.3 自动生成Page Objects
对于大型项目,可以使用工具自动生成初始Page类:
# utils/page_generator.py from selenium.webdriver.common.by import By def generate_page_class(page_name, elements): class_code = f"class {page_name}Page(BasePage):\n" for elem in elements: locator_type = getattr(By, elem['locator_type'].upper()) class_code += f" {elem['name'].upper()} = ({locator_type}, '{elem['value']}')\n" return class_code # 示例输入 elements = [ {"name": "search_box", "locator_type": "ID", "value": "search"}, {"name": "submit_btn", "locator_type": "XPATH", "value": "//button[@type='submit']"} ] print(generate_page_class("Search", elements))4. 常见问题排查指南
4.1 元素定位失效问题
现象:测试突然失败,报NoSuchElementException
排查步骤:
- 检查页面是否加载完成(添加page_load_wait)
- 验证定位器是否仍有效(使用浏览器开发者工具)
- 查看是否有iframe嵌套(需要switch_to.frame)
- 检查是否有动态ID(如"button-1234"中的数字部分)
解决方案:
# 在BasePage中添加智能等待方法 def smart_find(self, locator, attempts=3): for _ in range(attempts): try: return self.driver.find_element(*locator) except NoSuchElementException: self.driver.refresh() raise ElementNotFound(f"定位失败: {locator}")4.2 测试执行速度优化
瓶颈分析:
- 不必要的页面刷新(每次操作后都调用driver.get)
- 过度使用隐式等待(改为显式等待)
- 重复登录操作(使用session复用)
优化方案:
# conftest.py @pytest.fixture(scope="module") def auth_browser(browser): login_page = LoginPage(browser) login_page.quick_login(TEST_USER, TEST_PWD) yield browser # 不需要退出登录,节省时间 # 测试用例中使用module级别的fixture def test_checkout(auth_browser): product_page = ProductPage(auth_browser) # 直接开始操作,无需重复登录4.3 跨浏览器兼容问题
典型问题:
- Chrome正常但Firefox报错
- 元素在Headless模式下不可点击
应对策略:
# base_page.py def cross_browser_click(self, locator): try: self.click(locator) except WebDriverException: # Firefox备用方案 self.driver.execute_script( "arguments[0].click();", self.driver.find_element(*locator) )5. POM在复杂场景下的演进
5.1 移动端测试适配
当需要支持Appium进行移动测试时,可以创建MobileBasePage:
# mobile_base_page.py from appium.webdriver.common.touch_action import TouchAction class MobileBasePage(BasePage): def swipe_left(self): size = self.driver.get_window_size() TouchAction(self.driver)\ .press(x=size['width']*0.9, y=size['height']/2)\ .move_to(x=size['width']*0.1, y=size['height']/2)\ .release()\ .perform()5.2 可视化测试集成
结合Sikuli或Applitools进行视觉验证:
# pages/product_page.py class ProductPage(BasePage): PRODUCT_IMAGE = (By.CLASS_NAME, 'inventory_item_img') def verify_product_image(self): # 传统元素验证 assert self.driver.find_element(*self.PRODUCT_IMAGE).is_displayed() # 视觉验证(需要配置Applitools) eyes = Applitools() eyes.check_window("Product Image Visual Validation")5.3 AI元素定位辅助
使用机器学习模型辅助定位动态元素:
# ai_locator.py import cv2 import numpy as np class AILocator: def __init__(self, driver): self.driver = driver def find_by_image(self, template_path): # 获取当前屏幕截图 screenshot = self.driver.get_screenshot_as_png() screenshot = cv2.imdecode(np.frombuffer(screenshot, np.uint8), -1) # 读取模板图像 template = cv2.imread(template_path, 0) # 使用模板匹配 res = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) return max_loc if max_val > 0.8 else None6. 项目结构最佳实践
一个典型的POM项目目录结构:
├── config/ │ ├── browsers.yaml # 浏览器配置 │ └── environments.py # 环境变量 ├── pages/ │ ├── __init__.py │ ├── base_page.py │ ├── login_page.py │ └── components/ # 可重用组件 │ └── navbar.py ├── tests/ │ ├── __init__.py │ ├── test_login.py │ └── data/ # 测试数据 │ └── login_data.json ├── utils/ │ ├── report_utils.py # 报告生成 │ └── page_generator.py └── conftest.py # pytest fixtures关键配置示例(browsers.yaml):
chrome: headless: false options: - "--start-maximized" - "--disable-notifications" firefox: geckodriver_path: "/drivers/geckodriver" preferences: browser.download.dir: "/tmp/downloads"在conftest.py中读取配置:
import yaml from selenium import webdriver @pytest.fixture def browser(request): with open('config/browsers.yaml') as f: config = yaml.safe_load(f) if request.param == "chrome": options = webdriver.ChromeOptions() for opt in config['chrome']['options']: options.add_argument(opt) driver = webdriver.Chrome(options=options) yield driver driver.quit()7. 持续集成中的POM实践
7.1 Jenkins流水线配置
pipeline { agent any stages { stage('Test') { steps { script { def browsers = ['chrome', 'firefox'] browsers.each { browser -> parallel "${browser} Tests": { sh "pytest tests/ --browser=${browser} -n 4" } } } } post { always { allure serve allure-results } } } } }7.2 测试结果分析
在BasePage中添加智能日志记录:
def _log_action(self, action, locator=None, success=True): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") message = f"[{timestamp}] {action}" if locator: message += f" on {locator}" if not success: message += " - FAILED" # 写入日志文件 with open("test_actions.log", "a") as f: f.write(message + "\n") # 失败时截图 if not success: self.driver.save_screenshot(f"error_{timestamp}.png") # 在所有操作方法中调用 def click(self, locator): try: element = WebDriverWait(...) self._log_action("Click", locator) element.click() except Exception as e: self._log_action("Click", locator, False) raise8. 从POM到Screenplay模式
当项目规模扩大时,可以考虑更高级的Screenplay模式:
# abilities.py class BrowseTheWeb: def __init__(self, driver): self.driver = driver @staticmethod def using(driver): return BrowseTheWeb(driver) def find(self, locator): return WebDriverWait(self.driver, 10).until( EC.presence_of_element_located(locator) ) # tasks.py class Login: @staticmethod def as_user(username, password): return Task.where( "{0} logs in as {1}".format(Actor.named, username), Enter.the_value(username).into(LoginPage.USERNAME), Enter.the_value(password).into(LoginPage.PASSWORD), Click.on(LoginPage.SUBMIT_BTN) ) # 测试用例 def test_admin_login(actor): actor.who_can(BrowseTheWeb.using(browser)) actor.attempts_to( Login.as_user("admin", "secret") ) assert "dashboard" in browser.current_url这种模式的优点在于:
- 更贴近业务语言的测试描述
- 更好的任务复用性
- 更清晰的职责分离
9. 性能优化深度技巧
9.1 元素定位缓存
对于频繁访问的元素,可以添加缓存机制:
from functools import lru_cache class CachedPage(BasePage): @lru_cache(maxsize=32) def get_element(self, locator): return WebDriverWait(self.driver, 10).until( EC.presence_of_element_located(locator) ) # 使用 page.get_element(LoginPage.USERNAME).send_keys("user")9.2 并行测试策略
使用pytest-xdist实现并行化:
pytest tests/ -n 4 --browser=chrome对应的fixture改造:
@pytest.fixture def browser(request, worker_id): # 为每个worker创建独立下载目录 if worker_id != "master": download_dir = f"/tmp/downloads_{worker_id}" os.makedirs(download_dir, exist_ok=True) options.add_experimental_option("prefs", { "download.default_directory": download_dir })9.3 智能等待优化
动态调整等待时间:
def dynamic_wait(self, locator, base_timeout=10): start_time = time.time() while time.time() - start_time < base_timeout: try: return self.driver.find_element(*locator) except NoSuchElementException: # 根据已等待时间动态调整轮询间隔 elapsed = time.time() - start_time time.sleep(min(0.5, elapsed/10)) raise TimeoutException(f"元素 {locator} 未在 {base_timeout}秒内找到")10. 企业级POM框架设计
10.1 配置中心化
使用configparser管理多环境配置:
# config_loader.py import configparser class Config: def __init__(self, env): self.config = configparser.ConfigParser() self.config.read('config.ini') self.env = env @property def base_url(self): return self.config.get(self.env, 'base_url') # config.ini [dev] base_url = http://dev.example.com username = test_user [prod] base_url = https://example.com username = admin10.2 异常处理框架
自定义异常体系:
# exceptions.py class PageException(Exception): """基础页面异常""" class ElementNotFound(PageException): def __init__(self, locator): super().__init__(f"元素定位失败: {locator}") self.locator = locator class InvalidPageState(PageException): def __init__(self, current_url, expected): super().__init__( f"页面状态异常,当前: {current_url},预期包含: {expected}" )10.3 安全测试集成
在BasePage中添加安全检查点:
def security_check(self): # 检查控制台错误 logs = self.driver.get_log('browser') errors = [log for log in logs if log['level'] == 'SEVERE'] if errors: raise SecurityAlert(f"发现浏览器控制台错误: {errors}") # 检查可疑网络请求 performance_log = self.driver.get_log('performance') suspicious = [log for log in performance_log if 'malicious.com' in log['message']] if suspicious: raise SecurityAlert(f"发现可疑网络请求: {suspicious}")11. 测试报告增强
11.1 增强型Allure报告
# conftest.py import allure @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == 'call' and hasattr(item, 'instance'): # 添加页面截图 if hasattr(item.instance, 'driver'): allure.attach( item.instance.driver.get_screenshot_as_png(), name='screenshot', attachment_type=allure.attachment_type.PNG ) # 添加页面源码 allure.attach( item.instance.driver.page_source, name='page_source', attachment_type=allure.attachment_type.HTML )11.2 自定义HTML报告
使用pytest-html扩展:
# pytest.ini [pytest] addopts = --html=report.html --self-contained-html添加自定义样式:
<!-- 在conftest.py中添加 --> def pytest_html_report_title(report): report.title = "自动化测试报告 - POM框架" def pytest_html_results_summary(prefix, summary, postfix): prefix.extend([ html.h2("关键指标"), html.p(f"测试开始时间: {datetime.now()}") ])12. 测试数据管理
12.1 数据工厂模式
# data_factories/user_factory.py from faker import Faker class UserFactory: def __init__(self): self.fake = Faker() def create(self, role='standard'): base = { 'username': self.fake.user_name(), 'password': self.fake.password() } if role == 'admin': base.update({ 'permissions': ['create', 'delete'] }) return base # 测试中使用 def test_admin_flow(browser): admin = UserFactory().create(role='admin') LoginPage(browser).login_as(admin)12.2 测试数据版本控制
# utils/data_versioning.py import hashlib class DataVersion: def __init__(self, data_file): self.file = data_file self._hash = self._calculate_hash() def _calculate_hash(self): with open(self.file, 'rb') as f: return hashlib.md5(f.read()).hexdigest() def has_changed(self): return self._hash != self._calculate_hash() # 在fixture中使用 @pytest.fixture def test_data(): data = DataVersion('tests/data/login.json') if data.has_changed(): pytest.skip("测试数据已变更,请确认后重试") return json.load(open(data.file))13. 跨平台测试策略
13.1 统一API设计
# pages/universal_page.py class UniversalPage: def __init__(self, platform): self.platform = platform # 根据平台加载不同定位器 if platform == 'web': from .web_locators import LoginLocators self.locators = LoginLocators elif platform == 'mobile': from .mobile_locators import LoginLocators self.locators = LoginLocators def enter_username(self, text): if self.platform == 'web': self.driver.find_element(*self.locators.USERNAME).send_keys(text) else: # 移动端特殊处理 self.driver.hide_keyboard()13.2 平台适配层
# platforms/base_platform.py class PlatformAdapter: def click(self, element): raise NotImplementedError def input_text(self, element, text): raise NotImplementedError # platforms/web_platform.py class WebPlatform(PlatformAdapter): def click(self, element): element.click() def input_text(self, element, text): element.clear() element.send_keys(text) # platforms/mobile_platform.py class MobilePlatform(PlatformAdapter): def click(self, element): TouchAction(self.driver).tap(element).perform() def input_text(self, element, text): element.set_text(text)14. 测试环境治理
14.1 环境健康检查
# utils/environment.py def check_environment(): checks = [ _check_selenium_server, _check_browser_drivers, _check_test_data ] errors = [] for check in checks: try: check() except Exception as e: errors.append(str(e)) if errors: raise EnvironmentError("环境检查失败:\n" + "\n".join(errors)) def _check_selenium_server(): response = requests.get("http://localhost:4444/wd/hub/status") if not response.json()['value']['ready']: raise Exception("Selenium Server未就绪") # 在pytest启动时自动运行 def pytest_sessionstart(session): check_environment()14.2 测试数据清理
# fixtures/cleanup.py @pytest.fixture(scope="function") def clean_database(): # 测试前备份 db.backup() yield # 测试后恢复 db.restore() @pytest.fixture(scope="session", autouse=True) def final_cleanup(): yield # 所有测试完成后清理临时文件 for temp_file in glob.glob("/tmp/test_*.tmp"): os.unlink(temp_file)15. 测试代码质量保障
15.1 静态代码分析
在项目中添加.pre-commit-config.yaml:
repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - repo: https://github.com/psf/black rev: 22.3.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: - id: flake815.2 页面对象单元测试
# tests/unit/test_pages.py from unittest.mock import MagicMock class TestLoginPage: def test_login_success(self): # 创建模拟driver mock_driver = MagicMock() page = LoginPage(mock_driver) # 调用方法 page.enter_credentials("user", "pass") # 验证是否正确调用了底层方法 mock_driver.find_element.assert_any_call(*LoginPage.USERNAME) mock_driver.find_element().send_keys.assert_called_with("user")16. 团队协作规范
16.1 代码评审清单
创建.reviewchecklist.md:
### POM代码评审要点 - [ ] 页面类是否只包含该页面的元素和操作? - [ ] 是否有重复定位器可以抽象到父类? - [ ] 所有定位器是否使用常量定义? - [ ] 测试用例是否不包含直接的元素定位? - [ ] 是否合理使用了等待机制? - [ ] 是否有适当的异常处理? - [ ] 测试数据是否与代码分离?16.2 命名约定
制定团队命名规范:
- 页面类:
[功能]Page(如LoginPage、CheckoutPage) - 定位器:全大写+描述性名称(如
SUBMIT_BUTTON) - 测试方法:
test_[场景]_[预期](如test_invalid_login_shows_error) - 组件:
[功能]Component(如NavBarComponent)
17. 性能基准测试
17.1 关键操作耗时统计
# utils/performance.py import time from functools import wraps def measure_performance(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start # 记录到性能日志 with open("performance.log", "a") as f: f.write(f"{func.__name__},{elapsed:.4f}\n") return result return wrapper # 在关键方法上使用 class LoginPage(BasePage): @measure_performance def login(self, user, pwd): self.enter_credentials(user, pwd) self.submit()17.2 性能趋势分析
使用pytest-benchmark插件:
def test_login_performance(benchmark, browser): login_page = LoginPage(browser) benchmark(login_page.login, "perf_user", "test123")生成趋势图:
pytest --benchmark-histogram benchmarks/18. 安全测试扩展
18.1 XSS检测集成
# security/xss_checker.py XSS_PAYLOADS = [ "<script>alert('XSS')</script>", "javascript:alert(1)", "onerror=alert;" ] def check_xss_vulnerability(page, input_locator): vulnerabilities = [] for payload in XSS_PAYLOADS: page.enter_text(input_locator, payload) page.submit() if payload in page.get_page_source(): vulnerabilities.append(payload) return vulnerabilities18.2 CSRF防护验证
def verify_csrf_protection(page): # 获取原始token token1 = page.get_hidden_field("csrf_token") # 模拟会话固定攻击 page.driver.delete_all_cookies() # 再次获取token token2 = page.get_hidden_field("csrf_token") assert token1 != token2, "CSRF token未刷新,存在安全风险"19. 可视化测试工作台
19.1 元素关系图谱
使用graphviz生成页面元素关系图:
# utils/visualizer.py from graphviz import Digraph def generate_page_map(page_class): dot = Digraph() # 添加定位器节点 for name, locator in vars(page_class).items(): if name.isupper(): dot.node(name, f"{name}\n{locator[0]}: {locator[1]}") # 添加方法调用关系 for name, method in vars(page_class).items(): if callable(method) and not name.startswith('_'): dot.node(name, shape='box') # 分析方法源码找到使用的定位器 src = inspect.getsource(method) for locator in page_class.locators: if locator in src: dot.edge(name, locator) dot.render(f'docs/{page_class.__name__}.gv')19.2 测试流程动画
使用selenium-recorder生成操作回放:
# utils/recorder.py from selenium_recorder import Recorder class RecordedPage(BasePage): def __init__(self, driver): super().__init__(driver) self.recorder = Recorder(driver, output_dir="recordings") def click(self, locator): self.recorder.capture() super().click(locator) def save_recording(self, test_name): self.recorder.save(f"{test_name}.mp4")20. 人工智能增强测试
20.1 元素智能定位
# ai/element_locator.py import cv2 import numpy as np class AIElementLocator: def __init__(self, driver): self.driver = driver def find_by_visual(self, label): # 获取屏幕截图 screenshot = self.driver.get_screenshot_as_png() screenshot = cv2.imdecode(np.frombuffer(screenshot, np.uint8), -1) # 使用OCR识别文本 text_data = pytesseract.image_to_data(screenshot, output_type=pytesseract.Output.DICT) # 查找匹配的文本 for i, text in enumerate(text_data['text']): if label.lower() in text.lower(): # 返回元素位置 x = text_data['left'][i] y = text_data['top'][i] w = text_data['width'][i] h = text_data['height'][i] return (x + w//2, y + h//2) raise ElementNotFound(f"未找到包含文本'{label}'的元素")20.2 测试用例生成
使用自然语言处理生成测试场景:
# ai/test_generator.py from transformers import pipeline class TestGenerator: def __init__(self): self.generator = pipeline('text-generation', model='gpt-2') def generate_from_requirement(self, text): prompt = f""" 根据以下需求生成测试用例: 需求:{text} 测试用例名称:test_ """ output = self.generator(prompt, max_length=200) return self._parse_test_cases(output[0]['generated_text']) def _parse_test_cases(self, text): # 提取生成的测试用例 return [line.strip() for line in text.split('\n') if line.strip().startswith('test_')]21. 测试资产管理系统
21.1 定位器版本控制
# management/locator_repository.py import sqlite3 class LocatorRepository: def __init__(self): self.conn = sqlite3.connect('locators.db') self._create_table() def _create_table(self): self.conn.execute('''CREATE TABLE IF NOT EXISTS locators (id INTEGER PRIMARY KEY AUTOINCREMENT, page TEXT NOT NULL, name TEXT NOT NULL, by TEXT NOT NULL, value TEXT NOT NULL, version INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') def add_locator(self, page, name, by, value): # 检查是否已存在 cursor = self.conn.execute( "SELECT version FROM locators WHERE page=? AND name=? ORDER BY version DESC LIMIT 1", (page, name) ) result = cursor.fetchone() new_version = result[0] + 1 if result else 1 self.conn.execute( "INSERT INTO locators (page, name, by, value, version) VALUES (?, ?, ?, ?, ?)", (page, name, by, value, new_version) ) self.conn.commit() def get_locator(self, page, name, version=None): query = "SELECT by, value FROM locators WHERE page=? AND name=?" params = [page, name] if version: query += " AND version=?" params.append(version) else: query += " ORDER BY version DESC LIMIT 1" cursor = self.conn.execute(query, params) return cursor.fetchone()21.2 测试用例依赖图
# management/dependency_graph.py import networkx as nx import matplotlib.pyplot as plt class TestDependencyGraph: def __init__(self): self.graph = nx.DiGraph() def add_test_case(self, name, dependencies=None): self.graph.add_node(name) if dependencies: for dep in dependencies: self.graph.add_edge(dep, name) def visualize(self): pos = nx.spring_layout(self.graph) nx.draw(self.graph, pos, with_labels=True, node_size=2000, node_color="skyblue") plt.savefig("test_dependencies.png") def get_execution_order(self): return list(nx.topological_sort(self.graph))22. 测试框架扩展机制
22.1 插件系统设计
# framework/plugins.py from importlib import import_module from pathlib import Path class PluginManager: def __init__(self): self.plugins = {} def load_plugins(self, plugin_dir): for py_file in Path(plugin_dir).glob('*.py'): if py_file.name.startswith('_'): continue module_name = py_file.stem module = import_module(f"{plugin_dir}.{module_name}") if hasattr(module, 'register'): self.plugins[module_name] =