Pytest Web自动化测试实战:从环境搭建到工程化实践
1. 项目概述:为什么是Pytest?
如果你正在做Web自动化测试,或者打算从零开始搭建一个自动化测试框架,那么“Pytest”这个名字你肯定绕不过去。它早已不是Python测试领域的一个“新选择”,而是事实上的标准。我见过太多团队,从最初的unittest,到后来尝试nose,最终都迁移到了Pytest。为什么?因为它足够简单,又足够强大,能让你把精力真正放在测试逻辑和业务验证上,而不是跟框架本身较劲。
简单来说,Pytest是一个让编写和运行测试变得极其愉快的框架。它支持用简单的assert语句进行断言,自动发现测试用例,提供了丰富的插件生态(比如生成HTML报告、控制用例执行顺序、做分布式测试),并且它的fixture机制是解决测试数据准备和清理的“神器”。在Web自动化测试这个场景下,我们通常会将Pytest与Selenium、Playwright或Cypress等工具结合,用Pytest来组织、管理和运行我们的自动化测试脚本。
这篇文章,我会从一个有多年实战经验的测试开发者的角度,带你快速上手Pytest在Web自动化测试中的应用。我们不只讲语法,更会聚焦于如何用Pytest搭建一个健壮、可维护的Web自动化测试工程。你会学到如何组织目录结构、如何利用fixture管理浏览器驱动、如何实现数据驱动测试,以及如何生成漂亮的测试报告。目标很明确:让你看完就能动手,搭建起自己的第一个Pytest Web自动化测试项目。
2. 环境搭建与项目初始化
工欲善其事,必先利其器。在开始写第一个测试用例之前,我们需要先把环境准备好。一个清晰、标准的项目结构是后续一切高效工作的基础。
2.1 基础环境准备
首先,确保你的机器上已经安装了Python。我推荐使用Python 3.7及以上版本,Pytest对新版本Python的支持更好。你可以通过命令行检查:
python --version # 或 python3 --version接下来,安装Pytest。我强烈建议使用虚拟环境(venv)来管理项目依赖,这样可以避免不同项目之间的包版本冲突。
# 创建项目目录并进入 mkdir pytest-web-automation && cd pytest-web-automation # 创建虚拟环境(Windows用户使用 `python -m venv venv`) python3 -m venv venv # 激活虚拟环境 # macOS/Linux: source venv/bin/activate # Windows: venv\Scripts\activate # 安装pytest pip install pytest安装完成后,可以通过pytest --version来验证安装是否成功。
对于Web自动化,我们还需要浏览器驱动。这里以最经典的Selenium为例。我们需要安装selenium库,并下载对应浏览器的驱动(如ChromeDriver)。
# 安装selenium pip install selenium下载ChromeDriver时,务必注意浏览器版本与驱动版本的匹配,这是新手最容易踩的坑。去ChromeDriver官网下载与你的Chrome浏览器主版本号一致的驱动,下载后将其所在目录添加到系统的PATH环境变量中,或者直接放在项目目录下。
2.2 项目目录结构设计
一个混乱的目录结构是测试脚本难以维护的罪魁祸首。下面是我在实践中总结出的一个清晰、可扩展的目录结构,适合中小型Web自动化项目:
pytest-web-automation/ ├── conftest.py # Pytest的共享fixture配置 ├── pytest.ini # Pytest配置文件 ├── requirements.txt # 项目依赖列表 ├── test_cases/ # 存放所有测试用例 │ ├── __init__.py │ ├── test_login.py # 登录模块测试用例 │ └── test_search.py # 搜索模块测试用例 ├── page_objects/ # 页面对象模型(PO)目录 │ ├── __init__.py │ ├── base_page.py # 页面基类 │ ├── login_page.py # 登录页面类 │ └── search_page.py # 搜索页面类 ├── test_data/ # 测试数据文件 │ ├── login_data.json │ └── search_data.csv ├── reports/ # 测试报告输出目录 └── utils/ # 工具类目录 ├── __init__.py ├── logger.py # 日志工具 └── webdriver_helper.py # 浏览器驱动工具这样设计的好处:
- 职责分离:用例、页面对象、数据、工具各司其职,修改一个模块不会影响其他。
- 易于维护:当页面元素发生变化时,你只需要修改对应的
page_objects文件,所有用到该页面的测试用例都会自动生效。 - 便于集成:这样的结构很容易接入CI/CD(如Jenkins、GitLab CI),实现自动化触发测试。
现在,在项目根目录创建pytest.ini文件,这是Pytest的配置文件,可以让我们定制化测试行为。
[pytest] # 指定测试文件搜索的目录 testpaths = test_cases # 指定测试文件名的模式 python_files = test_*.py # 指定测试类名的模式 python_classes = Test* # 指定测试方法名的模式 python_functions = test_* # 添加命令行默认参数 addopts = -v --tb=short --strict-markers # 定义标记,防止未注册的标记被使用 markers = smoke: 冒烟测试用例 regression: 回归测试用例 slow: 执行较慢的用例这个配置告诉Pytest:去test_cases目录下,寻找以test_开头的.py文件,在这些文件中,寻找以Test开头的类,以及以test_开头的方法作为测试用例来执行。-v表示输出详细信息,--tb=short让错误回溯信息更简洁。
3. Pytest核心机制深度解析
要玩转Pytest,必须吃透它的几个核心机制:断言、Fixture、参数化和标记。理解了这些,你就能写出既简洁又强大的测试代码。
3.1 断言:告别繁琐的assert方法
Pytest最大的魅力之一就是它重写了Python自带的assert语句,使其能输出非常人性化的错误信息。你不再需要记忆unittest里各种各样的assertEqual,assertTrue等方法,一个assert走天下。
# 在unittest中你需要这样写: self.assertEqual(a, b) self.assertTrue(x) self.assertIn(item, list) # 在pytest中,你只需要: assert a == b assert x is True assert item in list当断言失败时,Pytest会给出清晰的对比信息。例如,assert “hello” == “world”,失败时会输出AssertionError: assert ‘hello’ == ‘world’,并高亮显示差异。对于复杂的对象比较,它也能进行深度对比并指出具体哪个属性不一致,这在大规模测试中排查问题时非常有用。
3.2 Fixture:测试的“脚手架”与依赖注入
Fixture是Pytest的灵魂,它用于为测试用例提供预设的上下文或环境。你可以把它想象成测试的“脚手架”或“后勤部长”。它的核心作用是setup(准备)和teardown(清理),并且支持作用域控制。
定义一个基础Fixture(管理浏览器):我们通常在conftest.py中定义项目级别的Fixture,这样所有测试文件都能使用。
# conftest.py import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options @pytest.fixture(scope="session") def browser(): """提供一个浏览器实例,整个测试会话只启动一次。""" # 初始化Chrome选项 chrome_options = Options() chrome_options.add_argument('--headless') # 无头模式,不打开GUI,适合CI环境 chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--disable-dev-shm-usage') # 创建驱动实例 driver = webdriver.Chrome(options=chrome_options) driver.implicitly_wait(10) # 设置隐式等待 driver.maximize_window() yield driver # 这是关键!将driver对象提供给测试用例使用 # 所有使用该fixture的测试执行完毕后,执行清理 driver.quit() print("浏览器已关闭。")关键点解析:
@pytest.fixture: 装饰器,声明这是一个fixture。scope=”session”: 定义fixture的作用域。可选function(默认,每个用例执行一次)、class、module、package、session。对于浏览器驱动,session级可以大幅提升测试速度,因为只需启动关闭一次浏览器。yield: 这是fixture的精髓。yield之前的代码是setup,yield返回的值(这里是driver)会注入给测试用例。测试用例执行完毕后,会回到这里执行yield之后的代码,即teardown。- 隐式等待:
implicitly_wait是一个全局设置,告诉WebDriver在查找元素时,如果元素没有立即出现,会等待一段时间(这里10秒)再去轮询查找。这比硬编码time.sleep要优雅和高效得多。
在测试用例中使用Fixture:只需在测试函数参数中声明同名的fixture即可。
# test_cases/test_sample.py def test_open_baidu(browser): # 参数名`browser`必须与fixture函数名一致 browser.get("https://www.baidu.com") assert "百度" in browser.titlePytest会自动调用browser()这个fixture,并将返回的driver对象传入测试函数。测试结束时,自动执行driver.quit()。
3.3 参数化:一键运行多组数据测试
参数化测试允许你使用不同的输入数据运行同一个测试逻辑,是数据驱动测试的基石。使用@pytest.mark.parametrize装饰器。
import pytest # 测试登录功能,使用多组用户名/密码 @pytest.mark.parametrize("username, password, expected", [ ("admin", "correct_password", True), # 正确密码,期望成功 ("admin", "wrong_password", False), # 错误密码,期望失败 ("", "some_password", False), # 用户名为空,期望失败 ("admin", "", False), # 密码为空,期望失败 ]) def test_login_with_params(username, password, expected, browser): # 假设login函数返回布尔值表示是否登录成功 login_page = LoginPage(browser) actual_result = login_page.login(username, password) assert actual_result == expected执行时,Pytest会将该测试函数展开成4个独立的测试用例来执行,并在报告中清晰区分。这极大地减少了代码重复,让测试覆盖更全面。
3.4 标记:灵活控制测试执行
标记(Mark)用于给测试用例分类,从而可以有选择地运行。我们在pytest.ini中已经定义了几个标记。
import pytest import time @pytest.mark.smoke # 标记为冒烟测试 def test_quick_check(browser): browser.get("https://www.example.com") assert browser.current_url == "https://www.example.com/" @pytest.mark.regression # 标记为回归测试 @pytest.mark.slow # 同时标记为慢速测试 def test_complex_workflow(browser): # 这是一个执行时间很长的复杂流程测试 time.sleep(5) # ... 复杂的测试步骤 assert True通过标记运行测试:
- 只运行冒烟测试:
pytest -m smoke - 运行除慢速测试外的所有用例:
pytest -m “not slow” - 同时运行冒烟和回归测试:
pytest -m “smoke or regression”
注意:使用自定义标记前,必须在
pytest.ini的markers项下声明,否则Pytest会发出警告(如果配置了--strict-markers则会报错)。这是一种防止标记名拼写错误的好习惯。
4. 整合Page Object Model (PO模型)
在Web自动化测试中,直接在被测页面上编写测试代码是“一次性”的,难以维护。页面元素一旦变化,所有相关测试脚本都得改。Page Object Model (PO模型) 正是为了解决这个问题而生的设计模式。它的核心思想是将页面封装成对象,页面的元素定位和操作细节都封装在对应的Page类中,测试脚本只调用Page对象提供的方法。
4.1 实现页面基类
首先,我们创建一个所有页面对象的基类BasePage,它封装了WebDriver和一些通用操作。
# page_objects/base_page.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(self.driver, 10) # 显式等待,更灵活 def find_element(self, locator): """查找单个元素,使用显式等待""" try: return self.wait.until(EC.presence_of_element_located(locator)) except TimeoutException: # 可以在这里加入日志记录或截图 print(f"元素未找到: {locator}") raise def find_elements(self, locator): """查找多个元素""" try: return self.wait.until(EC.presence_of_all_elements_located(locator)) except TimeoutException: print(f"元素组未找到: {locator}") return [] # 返回空列表,避免用例因找不到元素而直接中断 def click(self, locator): """点击元素""" element = self.find_element(locator) element.click() def input_text(self, locator, text): """向输入框输入文本""" element = self.find_element(locator) element.clear() element.send_keys(text) def get_text(self, locator): """获取元素的文本""" element = self.find_element(locator) return element.text def is_element_visible(self, locator, timeout=5): """判断元素是否可见""" try: WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) return True except TimeoutException: return False为什么用显式等待?隐式等待是全局的,对find_element和find_elements都生效,但它只检查元素是否存在(presence),不检查其状态(如是否可点击、是否可见)。显式等待更灵活,可以针对特定操作(如元素可点击、可见)进行等待,条件不满足时会抛出清晰的超时异常,更利于调试。
4.2 封装具体页面
以登录页面为例,我们创建一个LoginPage类。
# page_objects/login_page.py from selenium.webdriver.common.by import By from .base_page import BasePage class LoginPage(BasePage): # 1. 定义页面元素定位器(Locator) # 使用(By.策略, “值”)的元组形式,这是Selenium推荐的方式 USERNAME_INPUT = (By.ID, “username”) PASSWORD_INPUT = (By.ID, “password”) LOGIN_BUTTON = (By.XPATH, “//button[@type=‘submit’]”) ERROR_MESSAGE = (By.CLASS_NAME, “alert-error”) # 2. 定义页面操作方法 def open(self, url): self.driver.get(url) return self def enter_username(self, username): self.input_text(self.USERNAME_INPUT, username) return self # 返回self,支持链式调用 def enter_password(self, password): self.input_text(self.PASSWORD_INPUT, password) return self def click_login(self): self.click(self.LOGIN_BUTTON) # 3. 定义组合业务方法(供测试用例调用) def login(self, username, password): """完整的登录操作""" self.enter_username(username) self.enter_password(password) self.click_login() def get_error_message(self): """获取登录错误提示信息""" if self.is_element_visible(self.ERROR_MESSAGE): return self.get_text(self.ERROR_MESSAGE) return NonePO模型的核心优势:
- 高可维护性:如果登录按钮的定位器从
By.XPATH变成了By.CSS_SELECTOR,你只需要修改LOGIN_BUTTON这一个常量的值,所有调用click_login()的测试用例都无需改动。 - 高可读性:测试用例读起来就像业务文档:
login_page.login(“admin”, “123456”),非常清晰。 - 低耦合:页面操作细节被隐藏,测试脚本只关心业务流。
4.3 在测试用例中使用PO
现在,我们可以在测试用例中优雅地使用封装好的页面对象了。
# test_cases/test_login.py import pytest from page_objects.login_page import LoginPage class TestLogin: """登录功能测试类""" def test_successful_login(self, browser): """测试正常登录""" login_page = LoginPage(browser) login_page.open(“https://your-app.com/login”) login_page.login(“valid_user”, “valid_pass”) # 断言:登录成功后应跳转到首页,首页有用户菜单 # 这里假设首页有一个用户头像元素 assert browser.current_url == “https://your-app.com/dashboard” assert browser.find_element(By.ID, “user-avatar”).is_displayed() @pytest.mark.parametrize(“username, password”, [ (“invalid_user”, “valid_pass”), (“valid_user”, “”), (“”, “valid_pass”), ]) def test_failed_login_shows_error(self, browser, username, password): """测试各种登录失败场景,应显示错误信息""" login_page = LoginPage(browser) login_page.open(“https://your-app.com/login”) login_page.login(username, password) error_msg = login_page.get_error_message() # 断言错误信息存在且不为空 assert error_msg is not None assert len(error_msg) > 0 # 可以进一步断言错误信息的具体内容 # assert “用户名或密码错误” in error_msg可以看到,测试用例变得非常简洁和聚焦于业务验证。所有与页面元素交互的细节都被封装在LoginPage类中。
5. 高级技巧与工程化实践
掌握了基础之后,我们需要考虑如何让测试框架更健壮、更易用、更适合团队协作和持续集成。
5.1 测试数据分离
将测试数据从脚本中分离出来是良好实践。我们可以使用JSON、YAML、CSV甚至Excel来管理数据。这里以JSON为例。
// test_data/login_data.json { “valid_credentials”: { “username”: “standard_user”, “password”: “secret_sauce”, “expected_url”: “https://www.saucedemo.com/inventory.html” }, “invalid_credentials”: [ { “username”: “locked_out_user”, “password”: “secret_sauce”, “expected_error”: “Epic sadface: Sorry, this user has been locked out.” }, { “username”: “”, “password”: “secret_sauce”, “expected_error”: “Epic sadface: Username is required” } ] }然后,在Fixture或测试用例中读取这些数据。
import json import pytest @pytest.fixture(scope=“module”) def login_data(): with open(‘test_data/login_data.json’, ‘r’, encoding=‘utf-8’) as f: data = json.load(f) return data def test_valid_login(browser, login_data): data = login_data[“valid_credentials”] login_page = LoginPage(browser) login_page.open(“https://www.saucedemo.com/”) login_page.login(data[“username”], data[“password”]) assert browser.current_url == data[“expected_url”]5.2 失败截图与日志记录
测试失败时,一张截图抵得上千言万语。我们可以通过Pytest的钩子(hook)函数pytest_runtest_makereport来实现自动截图。
# conftest.py import pytest from datetime import datetime import os @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """ 获取每个测试用例执行结果的钩子函数 """ outcome = yield rep = outcome.get_result() # 获取测试报告对象 # 只关注测试用例执行(setup/call/teardown)中的‘call’阶段,即测试主体 if rep.when == “call” and rep.failed: # 获取测试用例中的browser fixture(需要确保测试用例使用了这个fixture) for fixture_name in item.fixturenames: if “browser” in fixture_name: browser = item.funcargs[fixture_name] break else: # 如果测试用例没有使用browser fixture,则跳过截图 return # 创建截图保存目录 screenshot_dir = “./reports/screenshots/” os.makedirs(screenshot_dir, exist_ok=True) # 生成带时间戳的截图文件名 timestamp = datetime.now().strftime(“%Y%m%d_%H%M%S”) test_name = item.name file_name = f”{test_name}_{timestamp}.png” file_path = os.path.join(screenshot_dir, file_name) # 截图 browser.save_screenshot(file_path) print(f”\n测试失败,截图已保存至: {file_path}”) # 也可以将截图路径附加到测试报告中(需要配合allure等报告插件) # if hasattr(rep, “extra”): # rep.extra.append(pytest_html.extras.image(file_path))同时,集成日志模块可以记录测试执行过程,方便回溯。
# utils/logger.py import logging import os def get_logger(name, level=logging.INFO): """获取一个配置好的logger实例""" # 创建logger logger = logging.getLogger(name) logger.setLevel(level) # 避免重复添加handler if not logger.handlers: # 创建控制台handler ch = logging.StreamHandler() ch.setLevel(level) # 创建文件handler log_dir = “./reports/logs/” os.makedirs(log_dir, exist_ok=True) fh = logging.FileHandler(os.path.join(log_dir, “automation.log”), encoding=‘utf-8’) fh.setLevel(level) # 定义输出格式 formatter = logging.Formatter( ‘%(asctime)s - %(name)s - %(levelname)s - %(message)s’ ) ch.setFormatter(formatter) fh.setFormatter(formatter) # 添加handler到logger logger.addHandler(ch) logger.addHandler(fh) return logger在页面对象或测试用例中引入日志。
# page_objects/base_page.py (补充) from utils.logger import get_logger class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(self.driver, 10) self.logger = get_logger(self.__class__.__name__) # 添加日志 def find_element(self, locator): try: self.logger.info(f”正在查找元素: {locator}”) element = self.wait.until(EC.presence_of_element_located(locator)) self.logger.info(f”元素查找成功: {locator}”) return element except TimeoutException: self.logger.error(f”元素查找超时: {locator}”) raise5.3 生成漂亮的HTML测试报告
命令行输出虽然直观,但一份结构化的HTML报告更利于分享和存档。pytest-html插件是首选。
# 安装插件 pip install pytest-html运行测试时指定生成报告:
pytest --html=./reports/report.html --self-contained-html--self-contained-html参数会将CSS样式内嵌到HTML中,生成单个文件,便于传输。报告会包含测试概述、通过/失败/跳过的用例列表、每个用例的执行时长以及我们之前钩子函数添加的截图(需要额外配置)。
为了获得更强大、更专业的报告(如趋势图、用例分类、附件管理等),可以考虑pytest-allure。Allure报告非常精美,但配置稍复杂。
5.4 并发执行测试用例
当测试用例成百上千时,顺序执行会非常耗时。Pytest可以通过pytest-xdist插件实现并行测试。
# 安装插件 pip install pytest-xdist运行测试时指定并行进程数:
pytest -n auto # 自动检测CPU核心数创建worker进程 # 或 pytest -n 4 # 指定启动4个worker进程重要注意事项:
- 资源竞争:并行测试时,多个用例可能同时操作浏览器或共享资源(如测试数据库)。需要确保你的测试用例是相互独立的,或者通过巧妙的Fixture作用域(如
scope=”session”的只读资源)和资源隔离来避免冲突。 - Session级Fixture:对于
scope=”session”的fixture(如我们的browserfixture),pytest-xdist默认会在每个worker进程中单独执行一次setup和teardown。如果你希望所有worker共享同一个浏览器会话(通常不推荐),需要更复杂的配置。
6. 常见问题排查与实战心得
在实际项目中,你会遇到各种各样的问题。这里我总结了一些高频问题和解决思路。
6.1 元素定位失败
这是Web自动化中最常见的问题,没有之一。
可能原因及解决方案:
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
NoSuchElementException | 1. 定位器写错了。 2. 页面尚未加载完成。 3. 元素在iframe或shadow DOM内。 4. 元素是动态生成的。 | 1.优先检查定位器:用浏览器开发者工具(F12)的Console标签,输入$x(‘你的XPath’)或$(‘你的CSS Selector’)验证。2.增加等待:使用显式等待( WebDriverWait)代替隐式等待或sleep,等待元素出现、可见或可点击。3.切换上下文:如果元素在iframe里,使用 driver.switch_to.frame(frame_reference)切换进去,操作完再switch_to.default_content()切回来。4.使用更稳定的定位策略:优先使用ID、Name,其次CSS Selector,最后才是XPath。避免使用包含索引(如 div[1])或绝对路径的XPath。 |
ElementNotInteractableException | 元素存在但不可交互(如被遮挡、未可见、disabled)。 | 1.等待元素可交互:使用EC.element_to_be_clickable(locator)。2.滚动到元素:使用 driver.execute_script(“arguments[0].scrollIntoView();”, element)。3.检查元素状态:确认元素没有 disabled属性,没有被其他元素(如弹窗、遮罩层)覆盖。 |
StaleElementReferenceException | 之前找到的元素,因为页面刷新或AJAX更新,已经“过时”了。 | 重新查找元素:这是最直接的解决办法。在PO模型中,每次操作前都通过定位器重新查找元素,而不是将找到的元素对象长期保存在变量中。 |
我的心得:永远不要依赖sleep。它让测试变得脆弱且缓慢。显式等待是解决动态加载问题的标准答案。同时,为关键操作(如点击、输入)编写重试机制也是一个提升稳定性的高级技巧。
6.2 测试用例独立性被破坏
测试用例之间相互影响,导致结果不稳定。
解决方案:
- 使用Fixture的
function作用域:确保每个测试用例都有全新的上下文。对于Web测试,如果每个用例都需要干净的浏览器状态,可以将browserfixture的作用域改为function,但会牺牲速度。 - 用例前置清理:在每个用例开始时,执行清理操作。例如,在登录测试前,先访问登出URL清理会话。
- 使用数据库隔离或Mock:对于依赖后端状态的测试,可以考虑在
setup中准备测试数据,在teardown中清理。或者使用Mock服务来模拟依赖。
6.3 在CI/CD中运行不稳定
在Jenkins、GitLab Runner等CI环境中,测试可能因为环境差异(无GUI、资源限制)而失败。
应对策略:
- 使用无头模式:如之前Fixture示例所示,添加
--headless参数。 - 增加超时时间:CI环境可能比本地慢,适当增加隐式等待和显式等待的超时时间。
- 确保环境一致性:使用Docker容器来运行测试,可以保证测试环境与CI环境完全一致。
- 处理随机弹窗:有些网站会有通知或广告弹窗。可以在Fixture启动浏览器后,执行一段JavaScript来禁用可能干扰测试的弹窗,或者提前将其关闭。
6.4 测试报告与结果分析
测试跑完了,如何快速定位问题?
- 利用
-v和-s参数:pytest -v -s可以输出最详细的执行信息,包括print语句和用例名称,方便本地调试。 - 只运行失败的用例:
pytest --lf(last-failed) 可以只重新运行上一次失败的用例。 - 使用
pytest-ordering控制顺序(谨慎使用):虽然测试用例理论上应该独立,但有时为了调试或满足特定业务流程,你可能需要控制执行顺序。可以使用@pytest.mark.run(order=1)装饰器,但这会破坏测试的独立性,应作为临时调试手段。
最后,我想分享一个最重要的心得:Web自动化测试的稳定性,只有30%取决于代码和框架,70%取决于被测应用本身的可测试性。与开发团队紧密合作,推动他们为关键元素添加稳定的、唯一的id或>
