1. 项目概述为什么我们需要一个“基础版”自动化测试框架在软件研发的日常里测试是保证质量的关键环节但也是最容易陷入重复劳动泥潭的地方。想象一下每次版本迭代你都需要手动点开几十个页面填写上百个表单点击上千次按钮只为了验证那些“理论上”不会出问题的老功能。这种工作不仅枯燥、低效而且极易出错尤其是在敏捷开发、持续交付的节奏下手工测试的瓶颈越来越明显。这就是自动化测试的价值所在——把重复、机械的验证工作交给机器让测试工程师和开发者能聚焦于更有创造性的探索性测试、业务逻辑设计和架构优化。而“UI/API 自动化测试框架基础版”这个标题精准地指向了自动化测试领域的两大核心阵地用户界面UI和应用程序接口API。一个基础版的框架其目标不是构建一个功能庞杂、学习曲线陡峭的“巨无霸”而是打造一个结构清晰、易于上手、便于扩展的基石。它应该能让一个有一定编程基础比如熟悉Python的测试人员或开发者在几天内跑通第一个自动化用例并理解其背后的运作逻辑。这个框架的价值在于“标准化”和“可复用”它定义了如何组织测试用例、如何管理测试数据、如何驱动浏览器或发送HTTP请求、如何生成报告等一系列通用问题的解决方案避免了每个项目都从零开始造轮子也避免了团队内每个人各写一套、风格迥异的混乱局面。从你提供的网络热词中我们可以看到大量关于Selenium、pytest、unittest、API Error的搜索这恰恰反映了实践中的痛点大家知道要用这些工具但在如何将它们有机组合、如何应对400 Bad Request、如何稳定地定位UI元素、如何管理测试环境等具体问题上缺乏一个现成的、经过验证的“脚手架”。一个优秀的基础框架正是为了解决这些共性问题而生。2. 框架核心架构设计分层与解耦一个健壮的自动化测试框架其核心思想是“分层”与“解耦”。这就像盖房子你不能把水管、电线、承重墙都混在一起浇筑。清晰的层次结构能让框架更易于维护、理解和扩展。对于一个同时涵盖UI和API测试的基础框架我推荐以下四层结构。2.1 驱动层与外界交互的“手和脚”这是框架最底层直接与测试目标浏览器或HTTP服务打交道。它的职责单一接收指令执行操作。对于UI测试我们通常选用Selenium WebDriver。它是一个W3C标准支持所有主流浏览器Chrome, Firefox, Edge等。它的原理是通过浏览器厂商提供的驱动程序如chromedriver向浏览器发送标准化命令如“打开URL”、“点击元素”、“输入文本”。在基础版框架中我们需要封装一个稳定的WebDriver管理类。# common/webdriver_manager.py from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.firefox.service import Service as FirefoxService from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.firefox import GeckoDriverManager import logging class WebDriverManager: _driver None classmethod def get_driver(cls, browser_namechrome, headlessFalse): 获取WebDriver实例单例模式避免重复创建 if cls._driver is None: if browser_name.lower() chrome: options webdriver.ChromeOptions() if headless: options.add_argument(--headless) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) # 适用于Linux环境 options.add_argument(--disable-dev-shm-usage) # 使用webdriver-manager自动管理驱动版本避免手动下载 service ChromeService(ChromeDriverManager().install()) cls._driver webdriver.Chrome(serviceservice, optionsoptions) elif browser_name.lower() firefox: options webdriver.FirefoxOptions() if headless: options.add_argument(--headless) service FirefoxService(GeckoDriverManager().install()) cls._driver webdriver.Firefox(serviceservice, optionsoptions) else: raise ValueError(fUnsupported browser: {browser_name}) cls._driver.implicitly_wait(10) # 设置隐式等待全局生效 cls._driver.maximize_window() return cls._driver classmethod def quit_driver(cls): 退出并清理WebDriver if cls._driver: cls._driver.quit() cls._driver None注意这里使用了webdriver-manager库它能自动下载和匹配当前浏览器版本的驱动是解决“驱动版本不匹配”这一经典坑点的利器。headless模式对于在服务器无图形界面上运行测试至关重要。对于API测试驱动层就是HTTP客户端。Requests库是Python中的事实标准简单易用且功能强大。我们需要封装一个发送请求、处理响应的基础客户端。# common/api_client.py import requests import json import logging class APIClient: def __init__(self, base_url): self.session requests.Session() # 使用Session保持会话如cookie self.base_url base_url self.logger logging.getLogger(__name__) def request(self, method, endpoint, **kwargs): 统一的请求方法 url f{self.base_url.rstrip(/)}/{endpoint.lstrip(/)} self.logger.info(fRequest: {method} {url}) if json in kwargs: self.logger.debug(fRequest Body: {json.dumps(kwargs[json], indent2, ensure_asciiFalse)}) try: response self.session.request(method, url, **kwargs) response.raise_for_status() # 如果状态码不是2xx抛出HTTPError异常 self.logger.info(fResponse Status: {response.status_code}) if response.content: self.logger.debug(fResponse Body: {response.text[:500]}...) # 日志截断避免过长 return response except requests.exceptions.RequestException as e: self.logger.error(fRequest failed: {e}) raise # 定义便捷方法 def get(self, endpoint, paramsNone, **kwargs): return self.request(GET, endpoint, paramsparams, **kwargs) def post(self, endpoint, dataNone, jsonNone, **kwargs): return self.request(POST, endpoint, datadata, jsonjson, **kwargs) def put(self, endpoint, dataNone, jsonNone, **kwargs): return self.request(PUT, endpoint, datadata, jsonjson, **kwargs) def delete(self, endpoint, **kwargs): return self.request(DELETE, endpoint, **kwargs)实操心得在APIClient中使用requests.Session()非常重要。Session对象会自动处理Cookies在需要登录态的API测试中你只需要登录一次后续请求都会携带认证信息模拟了真实用户的行为。response.raise_for_status()能快速发现非预期的HTTP状态码如404 500是API测试中断言的第一步。2.2 页面对象/接口对象层业务逻辑的“翻译官”这一层是框架设计的精髓它隔离了易变的UI元素/接口定义和稳定的测试逻辑极大地提升了测试脚本的可维护性。页面对象模型Page Object Model POM将每个UI页面抽象成一个类页面上的元素定位器如ID、XPath作为这个类的属性页面的操作如登录、搜索作为这个类的方法。这样当页面UI发生改变时你只需要修改这一个类中的元素定位器所有用到该页面的测试用例都无需改动。# page_objects/login_page.py from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LoginPage: # 元素定位器 USERNAME_INPUT (By.ID, username) PASSWORD_INPUT (By.ID, password) LOGIN_BUTTON (By.ID, submit) ERROR_MSG (By.CLASS_NAME, error-message) def __init__(self, driver): self.driver driver self.wait WebDriverWait(driver, 10) def load(self, url): self.driver.get(url) return self def enter_username(self, username): # 使用显式等待确保元素可交互 element self.wait.until(EC.element_to_be_clickable(self.USERNAME_INPUT)) element.clear() element.send_keys(username) return self def enter_password(self, password): element self.wait.until(EC.element_to_be_clickable(self.PASSWORD_INPUT)) element.clear() element.send_keys(password) return self def click_login(self): element self.wait.until(EC.element_to_be_clickable(self.LOGIN_BUTTON)) element.click() return self def get_error_message(self): 获取登录错误提示信息 try: element self.wait.until(EC.visibility_of_element_located(self.ERROR_MSG)) return element.text except: return None def login(self, username, password): 链式调用完成完整登录流程 return self.enter_username(username).enter_password(password).click_login()注意事项POM类中的方法通常返回self或页面对象本身这支持了链式调用page.login().do_something()让测试代码更流畅。显式等待WebDriverWait比隐式等待更可靠它针对特定条件进行等待避免了不必要的全局等待时间。接口对象模型类似POM我们可以为每一组相关的API如用户管理、订单管理创建一个类将接口路径、默认请求头、通用的参数处理封装在里面。# api_objects/user_api.py from common.api_client import APIClient class UserAPI: def __init__(self, client: APIClient): self.client client def login(self, username, password): 登录接口 endpoint /api/v1/auth/login payload {username: username, password: password} return self.client.post(endpoint, jsonpayload) def get_user_profile(self, user_id): 获取用户资料需要认证 endpoint f/api/v1/users/{user_id} return self.client.get(endpoint) def create_user(self, user_data): 创建新用户 endpoint /api/v1/users return self.client.post(endpoint, jsonuser_data)2.3 测试用例层定义“做什么”和“期望什么”这一层是测试逻辑的具体实现。它调用页面对象或接口对象的方法组织测试步骤并添加断言Assertion来验证结果是否符合预期。我们使用pytest或unittest这样的测试框架来组织用例。pytest因其简洁的语法、强大的夹具fixture功能和丰富的插件生态已成为Python自动化测试的主流选择。下面是一个使用pytest和上面定义的LoginPage的UI测试用例示例。# tests/ui/test_login.py import pytest from common.webdriver_manager import WebDriverManager from page_objects.login_page import LoginPage class TestLogin: pytest.fixture(scopeclass) def driver(self): Fixture: 为整个测试类提供唯一的driver实例 driver WebDriverManager.get_driver(chrome, headlessTrue) # 测试时用headless模式 yield driver # 测试类结束后清理注意这里不quit由WebDriverManager统一管理便于用例间复用浏览器 pytest.fixture def login_page(self, driver): Fixture: 为每个测试方法提供一个新的LoginPage实例 page LoginPage(driver) page.load(https://your-app.com/login) return page def test_login_success(self, login_page): 测试正常登录 # 执行操作 login_page.login(valid_user, valid_password) # 断言登录后应跳转到首页首页包含用户信息 # 这里假设首页标题包含“Dashboard” assert Dashboard in login_page.driver.title # 或者更精确地等待某个登录成功后的特定元素出现 # assert login_page.wait_for_element(HomePage.USER_AVATAR).is_displayed() def test_login_with_wrong_password(self, login_page): 测试密码错误 login_page.login(valid_user, wrong_password) error_msg login_page.get_error_message() # 断言应出现错误提示且提示内容正确 assert error_msg is not None assert 密码错误 in error_msg or invalid credentials in error_msg.lower() pytest.mark.parametrize(username, password, [ (, somepassword), # 用户名为空 (someuser, ), # 密码为空 (scriptalert(1)/script, pass), # XSS尝试 ]) def test_login_validation(self, login_page, username, password): 参数化测试测试各种边界和无效输入 login_page.login(username, password) error_msg login_page.get_error_message() assert error_msg is not None # 期望有验证错误提示对于API测试用例写法类似但更关注HTTP状态码和响应体。# tests/api/test_user_api.py import pytest from common.api_client import APIClient from api_objects.user_api import UserAPI class TestUserAPI: pytest.fixture def api_client(self): return APIClient(base_urlhttps://your-api.com) pytest.fixture def user_api(self, api_client): return UserAPI(api_client) def test_login_success(self, user_api): 测试API登录成功 response user_api.login(testuser, correct_password) # 断言1状态码为200 assert response.status_code 200 # 断言2响应体包含token字段 json_data response.json() assert token in json_data assert isinstance(json_data[token], str) and len(json_data[token]) 10 # 断言3token类型正确例如JWT通常以特定前缀开头 # assert json_data[token].startswith(eyJ) # JWT示例 def test_login_failure(self, user_api): 测试API登录失败 response user_api.login(wronguser, wrongpassword) # 断言状态码应为401未授权或400错误请求 assert response.status_code in [400, 401] json_data response.json() assert message in json_data assert invalid in json_data[message].lower() or error in json_data[message].lower() def test_get_profile_unauthorized(self, user_api): 测试未授权访问用户资料 # 注意这个测试中user_api的client没有登录所以没有token response user_api.get_user_profile(1) assert response.status_code 401 # 未认证核心技巧pytest.mark.parametrize装饰器是数据驱动测试的利器。它允许你用多组数据运行同一个测试方法极大地减少了代码重复。对于UI测试可以用来测试不同用户角色的登录对于API测试可以用来测试各种边界值的输入。2.4 测试数据与配置层让测试“活”起来测试数据如用户名、密码、商品ID和运行配置如测试环境URL、数据库连接串不应该硬编码在测试用例中。将它们分离出来可以使测试更灵活更容易在不同环境开发、测试、预生产间切换。使用配置文件推荐使用configparser读取.ini文件或者直接使用yaml、json文件。; config.ini [environments] dev.base_url https://dev.your-app.com test.base_url https://test.your-app.com [browser] name chrome headless True window_width 1920 window_height 1080 [api] default_timeout 30# common/config.py import configparser import os class Config: _instance None def __new__(cls): if cls._instance is None: cls._instance super().__new__(cls) cls._instance.load_config() return cls._instance def load_config(self): self.config configparser.ConfigParser() # 可以设置环境变量来决定加载哪个配置文件 env os.getenv(TEST_ENV, test) config_file fconfig.{env}.ini if os.path.exists(fconfig.{env}.ini) else config.ini self.config.read(config_file, encodingutf-8) def get(self, section, key, fallbackNone): return self.config.get(section, key, fallbackfallback) def getboolean(self, section, key, fallbackFalse): return self.config.getboolean(section, key, fallbackfallback) # 使用方式 config Config() base_url config.get(environments, test.base_url) headless config.getboolean(browser, headless)管理测试数据对于简单的数据可以用JSON或YAML文件。对于复杂的数据关系可以考虑使用CSV或Excel甚至连接测试数据库。一个常见的模式是将测试用例ID与对应的输入、预期输出关联起来。// test_data/login_data.json [ { case_id: LOGIN_001, username: standard_user, password: secret_sauce, expected_result: success, expected_title_contains: Swag Labs }, { case_id: LOGIN_002, username: locked_out_user, password: secret_sauce, expected_result: failure, expected_error_msg: Sorry, this user has been locked out. } ]# common/data_loader.py import json import pytest def load_test_data_from_json(file_path): with open(file_path, r, encodingutf-8) as f: return json.load(f) # 在测试用例中使用 pytest.mark.parametrize(test_data, load_test_data_from_json(test_data/login_data.json)) def test_login_with_data(login_page, test_data): login_page.login(test_data[username], test_data[password]) if test_data[expected_result] success: assert test_data[expected_title_contains] in login_page.driver.title else: error_msg login_page.get_error_message() assert error_msg and test_data[expected_error_msg] in error_msg3. 核心环节实现让框架真正跑起来有了清晰的分层设计接下来就是将这些部分串联起来并解决一些工程化问题比如测试报告、失败重试、并发执行等。3.1 测试执行与报告生成pytest本身可以通过-v、-s等参数输出控制台报告但对于自动化测试一份美观、信息丰富的HTML报告是团队协作和问题追溯的必需品。pytest-html和allure-pytest是两个主流选择。pytest-html更轻量开箱即用Allure更强大支持步骤step描述、附件截图、日志等高级功能。这里以集成pytest-html和自动截图为例首先安装pip install pytest-html然后我们可以创建一个conftest.py文件pytest会自动发现该文件并加载其中的钩子函数来配置报告和实现失败自动截图。# conftest.py import pytest from datetime import datetime from common.webdriver_manager import WebDriverManager import os pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): 钩子函数在每个测试步骤执行后获取测试报告状态。 如果测试失败则截取屏幕截图并附加到HTML报告中。 outcome yield report outcome.get_result() # 只关心测试用例本身的调用阶段setup, call, teardown中的call if report.when call and report.failed: # 确保截图目录存在 screenshot_dir reports/screenshots os.makedirs(screenshot_dir, exist_okTrue) # 生成唯一的截图文件名 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) test_name item.name screenshot_path os.path.join(screenshot_dir, f{test_name}_{timestamp}.png) # 获取当前活动的driver假设通过fixture传递 # 这里需要根据你的fixture名称调整例如‘driver’ driver_fixture item.funcargs.get(driver) if driver_fixture: try: driver_fixture.save_screenshot(screenshot_path) # 将截图路径添加到测试报告的extra字段pytest-html会识别 if hasattr(report, extra): # 确保extra是列表 from pytest_html import extras report.extra report.extra if report.extra else [] report.extra.append(extras.image(screenshot_path)) report.extra.append(extras.html(fdiva href{screenshot_path} target_blank查看截图/a/div)) except Exception as e: print(fFailed to take screenshot: {e}) pytest.fixture(scopesession, autouseTrue) def manage_driver(): 会话级别的fixture用于在所有测试开始前和结束后管理driver。 这样可以确保所有测试用例在一个浏览器会话中运行如果不需要隔离的话提高速度。 # 这里可以进行一些全局初始化比如读取配置 print(Test session started.) yield # 所有测试结束后退出浏览器 WebDriverManager.quit_driver() print(Test session finished. Browser closed.)运行测试并生成报告# 运行所有测试生成HTML报告 pytest tests/ --htmlreports/report.html --self-contained-html # 运行指定标记的测试 pytest tests/ -m smoke --htmlreports/smoke_report.html # 详细输出显示每个测试的名字 pytest tests/ -v生成的report.html会包含测试通过/失败的状态、执行时间、错误追溯以及我们附加的失败截图链接一目了然。3.2 测试用例的组织与标记当测试用例成百上千时如何高效地组织和管理它们pytest的标记mark功能是关键。按功能模块这是最自然的方式将同一功能如登录、购物车、支付的测试用例放在同一个目录或Python模块下。按测试类型使用pytest.mark.ui、pytest.mark.api来区分UI和API测试。按测试级别使用pytest.mark.smoke冒烟测试、pytest.mark.regression回归测试来标记用例的重要性和执行频率。按缺陷关联使用pytest.mark.bug(“JIRA-123”)关联到具体的缺陷单。首先在项目根目录或tests目录下创建一个pytest.ini文件来注册这些标记避免运行时出现警告。; pytest.ini [pytest] markers smoke: 冒烟测试用例 regression: 回归测试用例 ui: UI自动化测试 api: API自动化测试 slow: 运行缓慢的测试 bug: 关联到已知缺陷 addopts -v --strict-markers --tbshort然后在测试用例上使用标记# tests/ui/test_cart.py import pytest pytest.mark.ui pytest.mark.smoke class TestShoppingCart: def test_add_item_to_cart(self): pass pytest.mark.regression def test_remove_item_from_cart(self): pass pytest.mark.ui pytest.mark.slow # 标记为慢速测试可以单独跳过 def test_checkout_complex_flow(): pass运行命令就变得非常灵活# 只运行冒烟测试 pytest -m smoke # 运行UI相关的回归测试 pytest -m ui and regression # 运行除了慢速测试外的所有测试 pytest -m not slow # 运行特定文件中的特定类 pytest tests/ui/test_cart.py::TestShoppingCart3.3 持续集成CI集成自动化测试只有融入CI/CD流水线才能最大化其价值。每次代码提交或定时构建时自动触发测试套件快速反馈质量情况。这里以GitHub Actions为例展示一个简单的CI配置。# .github/workflows/run-tests.yml name: Python UI/API Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] schedule: - cron: 0 2 * * * # 每天凌晨2点运行一次 jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [‘3.9’, ‘3.10’] # 测试多个Python版本 steps: - uses: actions/checkoutv3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-pythonv4 with: python-version: ${{ matrix.python-version }} - name: Install system dependencies (for Chrome) run: | sudo apt-get update sudo apt-get install -y wget unzip wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - echo deb [archamd64] http://dl.google.com/linux/chrome/deb/ stable main | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt-get update sudo apt-get install -y google-chrome-stable - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run UI Tests (Headless Chrome) env: TEST_ENV: ci # 使用CI环境的配置文件 run: | pytest tests/ui/ -m not slow --htmlreports/ui-report-${{ matrix.python-version }}.html --self-contained-html - name: Run API Tests env: TEST_ENV: ci run: | pytest tests/api/ --htmlreports/api-report-${{ matrix.python-version }}.html --self-contained-html - name: Upload test reports if: always() # 即使测试失败也上传报告 uses: actions/upload-artifactv3 with: name: test-reports-py${{ matrix.python-version }} path: reports/这个工作流做了几件事1) 在Ubuntu最新环境中运行2) 安装Chrome浏览器和驱动3) 安装Python依赖4) 分别运行UI和API测试跳过标记为slow的用例5) 将生成的HTML报告上传为构件供后续下载查看。4. 常见问题与避坑指南实录在实际搭建和使用框架的过程中你会遇到无数个坑。下面是我总结的一些高频问题和解决方案。4.1 UI自动化中的“元素定位”难题问题NoSuchElementException是UI自动化中最常见的异常。页面元素加载慢、元素在iframe内、元素属性动态变化等都可能导致定位失败。解决方案与技巧优先使用稳定的定位器定位策略的优先级建议为ID Name CSS Selector XPath。ID通常是唯一且最稳定的。尽量避免使用包含索引位置如div[3]或复杂文本的XPath它们极易因前端微调而失效。显式等待是王道永远不要使用time.sleep()使用WebDriverWait配合expected_conditions。# 反面教材 time.sleep(5) # 死等5秒可能浪费也可能不够 element driver.find_element(By.ID, ‘dynamic-element’) # 正面教材 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) # 最多等10秒 # 等待元素可见并可点击 element wait.until(EC.element_to_be_clickable((By.ID, ‘dynamic-element’))) # 等待元素存在可能在DOM但不可见 element wait.until(EC.presence_of_element_located((By.ID, ‘dynamic-element’)))处理动态ID/Class如果元素的ID是类似button-12345这样带随机后缀的可以使用CSS选择器的部分匹配^开头$结尾*包含。# 匹配ID以‘button-’开头的元素 driver.find_element(By.CSS_SELECTOR, “[id^‘button-’]”) # 匹配Class包含‘primary-btn’的元素 driver.find_element(By.CSS_SELECTOR, “[class*‘primary-btn’]”)切换到iframe如果元素在iframe里必须先切换到对应的iframe。# 通过ID或Name切换 driver.switch_to.frame(‘iframe_id_or_name’) # 通过索引切换不推荐 # driver.switch_to.frame(0) # 通过WebElement切换 # iframe_element driver.find_element(By.TAG_NAME, ‘iframe’) # driver.switch_to.frame(iframe_element) # 操作完成后切回主文档 driver.switch_to.default_content()4.2 API测试中的“状态管理”与“数据清理”问题API测试常常是有状态的。比如测试“创建订单”接口会改变系统状态。如何保证测试的独立性和可重复性测试数据如创建的用户、订单如何清理解决方案测试夹具Fixture管理生命周期pytest的fixture的scope参数function,class,module,session可以控制setup/teardown的范围。对于需要清理的资源一定要在fixture的teardown部分处理。import pytest pytest.fixture def test_user(api_client): 创建一个测试用户测试完成后删除它 user_api UserAPI(api_client) # Setup: 创建用户 user_data {‘name’: ‘TestUser’, ‘email’: ‘testexample.com’} create_resp user_api.create_user(user_data) user_id create_resp.json()[‘id’] yield user_id # 将user_id提供给测试用例使用 # Teardown: 删除用户无论测试成功还是失败都会执行 try: user_api.delete_user(user_id) except: pass # 记录日志但不要因为清理失败而掩盖测试本身的问题使用测试专用环境和数据为自动化测试准备独立的数据库或使用可以快速重置的环境如Docker容器。测试数据使用特定前缀便于识别和批量清理。# 在conftest.py中设置 import os import pytest pytest.fixture(scope‘session’, autouseTrue) def setup_test_environment(): env os.getenv(‘TEST_ENV’, ‘test’) # 这里可以执行一些环境准备脚本比如运行数据库迁移、导入基础数据 print(f“Setting up {env} environment...”) yield # 测试会话结束后可以执行清理脚本 print(“Tearing down environment...”)接口依赖处理测试B接口需要A接口返回的token。不要在测试B的用例里再去调用A而是通过fixture来获取这个依赖。pytest.fixture def auth_token(api_client): user_api UserAPI(api_client) resp user_api.login(‘test_user’, ‘test_pass’) return resp.json()[‘token’] pytest.fixture def authenticated_client(api_client, auth_token): 返回一个已经设置了认证头的client api_client.session.headers.update({‘Authorization’: f’Bearer {auth_token}’}) return api_client def test_access_protected_resource(authenticated_client): # 这个client已经带token了 response authenticated_client.get(‘/api/protected’) assert response.status_code 2004.3 测试的“稳定性”与“ flaky tests”问题有些测试用例时而成功时而失败被称为“flaky tests”。它们严重损害测试套件的可信度。常见原因包括网络波动、第三方服务不稳定、时间依赖如等待特定时间点、测试间状态污染。排查与解决思路增加重试机制对于因网络或瞬时负载导致的失败可以增加重试。pytest有pytest-rerunfailures插件。pip install pytest-rerunfailures pytest --reruns 3 --reruns-delay 2 # 失败后重试3次每次间隔2秒注意重试是缓解手段不是根本解决方案。应优先定位不稳定的根源。彻底隔离测试状态确保每个测试用例都是独立的。使用scope‘function’的fixture或者在每个测试开始前显式地清理状态如清空浏览器cookies、localStorageAPI测试则清理创建的数据。移除硬编码等待使用智能等待如前所述用显式等待替代time.sleep。Mock外部依赖对于调用第三方支付、短信网关等不可控外部服务的测试使用Mock模拟对象来替代真实调用。Python的unittest.mock模块非常强大。from unittest.mock import patch, MagicMock def test_order_payment_success(): # 模拟支付网关的响应使其始终返回成功 with patch(‘your_module.PaymentGateway.charge’) as mock_charge: mock_charge.return_value {‘status’: ‘succeeded’, ‘transaction_id’: ‘mock_123’} # 调用你的业务逻辑它会使用被mock的支付网关 result process_order(‘order_123’) assert result[‘payment_status’] ‘paid’ # 还可以断言你的代码是否以正确的参数调用了支付接口 mock_charge.assert_called_once_with(amount100.0, currency‘USD’)记录详细的日志和截图在测试失败时除了截图还应该记录浏览器控制台日志、网络请求、测试执行上下文等信息这些是排查flaky test的宝贵线索。pytest可以配合logging模块将日志输出到文件。4.4 框架的“可维护性”与“扩展性”问题随着项目发展测试用例越来越多框架变得臃肿修改一个公共方法可能影响上百个用例。维护建议严格遵守POM和分层原则这是维护性的基石。UI变更只改Page Object接口变更只改API Object。使用公共工具类将常用的操作封装成工具函数如日期处理、字符串生成、文件读写、数据库查询等放在common/utilities.py中。版本控制测试代码将测试代码与产品代码放在同一个仓库或子模块中使用相同的分支策略。这样测试代码的修改可以与产品功能的开发同步进行和评审。编写清晰的文档和示例在项目根目录维护一个README.md说明如何搭建环境、运行测试、编写新用例、目录结构含义。在examples/目录下放置典型场景的测试用例代码。定期重构像对待产品代码一样对待测试代码。定期回顾删除重复代码优化低效的定位器更新过时的依赖库。搭建一个“基础版”的UI/API自动化测试框架远不是把几个库拼在一起那么简单。它需要你像设计一个软件系统一样去思考其架构、可维护性和扩展性。这个框架的终极目标是成为团队信任的“质量守门员”能够快速、可靠、清晰地反馈每次变更对系统的影响。从今天分享的这个基础骨架出发你可以根据自己项目的具体需求逐步添砖加瓦比如集成数据库验证、加入性能监控、搭建测试用例管理系统等让它不断进化真正为研发效能保驾护航。记住好的框架是“活”的它应该随着项目和团队一起成长。