1. 项目概述:当Selenium遇上Pytest,自动化测试的化学反应
如果你正在做Web自动化测试,或者正准备踏入这个领域,那么“Selenium+Pytest”这个组合对你来说,绝对不是一个陌生的词汇。但很多时候,我们只是把它们当作两个独立的工具在用:Selenium负责在浏览器里“点点点”,Pytest负责组织和管理我们的测试代码。这就像你有一把锋利的瑞士军刀(Selenium)和一个设计精良的工具箱(Pytest),却只是把刀扔在箱子里,没有真正组合起来去完成一件精密的活儿。
那么,当Selenium的浏览器驱动能力与Pytest的测试组织、执行和报告能力深度融合,会碰撞出什么样的火花?这远不止是“1+1=2”的简单叠加。更具体一点,很多新手会困惑:我直接用Selenium的webdriver写脚本,和用Pytest框架来组织这些脚本,到底有什么区别?前者是不是就算自动化了?后者又带来了什么质变?今天,我们就来彻底拆解这个组合,看看它如何从一套“脚本集合”进化成一个真正的、可维护、可扩展的“自动化测试框架”。
简单来说,仅使用Selenium WebDriver,你得到的是自动化测试的能力——模拟用户操作浏览器。而引入Pytest,你构建的是自动化测试的工程体系——它解决了测试用例如何被高效地组织、执行、参数化、报告以及融入持续集成流程等一系列工程化问题。前者是“砖瓦”,后者是“建筑蓝图和施工规范”。接下来,我将结合我多年的实战经验,带你从设计思路到实操细节,完整走一遍构建一个健壮的Selenium+Pytest框架的旅程,并厘清其中每一个关键选择背后的“为什么”。
2. 框架设计核心思路:从“脚本”到“工程”的跨越
2.1 为何是Pytest,而不是unittest或纯脚本?
在Python的测试生态中,unittest是标准库,而Pytest是第三方框架。选择Pytest作为核心,是基于几个压倒性的优势,这些优势在构建中大型自动化项目时尤为明显。
第一,极简的语法和强大的灵活性。unittest要求测试类必须继承unittest.TestCase,测试方法必须以test_开头。Pytest则宽松得多,它也能识别test_开头的函数和类中的test_方法,但它没有强制继承的要求。这意味着你的测试代码更干净,更贴近普通的Python代码。例如,一个简单的Pytest测试用例,看起来就像这样:
# test_login.py def test_user_login_with_valid_credentials(): driver = webdriver.Chrome() driver.get("https://example.com/login") # ... 操作步骤 assert "Dashboard" in driver.title driver.quit()没有类,没有继承,直接就是一个函数。这种简洁性降低了学习成本和代码冗余。
第二,强大的Fixture机制。这是Pytest的灵魂,也是它与Selenium结合的关键。Fixture用于为测试用例提供预设的上下文和环境。对于Web自动化,最典型的Fixture就是浏览器驱动(WebDriver)的初始化与清理。你可以这样定义一个driverFixture:
# conftest.py import pytest from selenium import webdriver @pytest.fixture(scope="function") def driver(): # 测试开始前,初始化浏览器 options = webdriver.ChromeOptions() options.add_argument('--headless') # 无头模式,适合CI环境 options.add_argument('--disable-gpu') options.add_argument('--no-sandbox') driver = webdriver.Chrome(options=options) driver.implicitly_wait(10) # 隐式等待 yield driver # 将driver对象提供给测试用例使用 # 测试结束后,无论成功失败,都关闭浏览器 driver.quit()然后在测试用例中,你只需要将driver作为参数传入,Pytest会自动注入这个已经初始化好的对象:
def test_search(driver): # Pytest会自动注入conftest.py中定义的driver fixture driver.get("https://www.baidu.com") search_box = driver.find_element(By.ID, 'kw') search_box.send_keys('Pytest') search_box.submit() assert 'pytest' in driver.page_source.lower()这种方式实现了资源的精准管理和复用。scope参数可以控制Fixture的生命周期(如function每个用例执行一次,class每个类一次,session整个测试会话一次),完美解决了浏览器反复启动关闭的性能损耗问题。
第三,丰富的插件生态与出色的报告。Pytest有诸如pytest-html(生成HTML报告)、pytest-xdist(分布式并行测试)、pytest-rerunfailures(失败重试)等海量插件。特别是与Allure框架的集成,可以生成极其美观、信息丰富的交互式测试报告,这对于测试结果分析和团队协作至关重要。而原生的unittest或纯Selenium脚本要实现同等效果的报告,需要投入大量的额外开发工作。
第四,参数化测试和标记(Mark)功能。参数化让你能轻松用多组数据驱动同一个测试逻辑。标记功能则可以灵活地挑选或跳过某些测试用例(如标记为@pytest.mark.smoke进行冒烟测试)。
实操心得:在项目初期,可能觉得用
if __name__ == "__main__"来执行几个Selenium脚本也挺快。但当用例数超过50个,涉及多种浏览器、多种测试环境,需要生成报告、集成到Jenkins时,缺乏框架支撑的脚本集合会迅速变成难以维护的“泥球”。Pytest提供的是一套现成的、最佳实践的工程解决方案,从第一天开始就采用它,是避免后期重构痛苦的关键决策。
2.2 PO模型:框架可维护性的基石
仅仅引入Pytest管理执行还不够。如果所有页面定位和操作逻辑都散落在各个测试用例中,一旦页面元素ID或结构发生变化,你需要修改所有相关的用例——这将是维护的噩梦。这时,Page Object (PO) 模型就必须登场了。
PO模型的核心思想是将页面封装成对象,页面的元素定位和基本操作作为对象的方法,测试用例则通过调用这些方法来完成业务流,而不直接操作WebDriver。这样,页面元素的变动只需要在对应的PO类中修改一次。
一个典型的PO模型目录结构如下:
project_root/ ├── conftest.py # Pytest全局配置和Fixture定义 ├── pytest.ini # Pytest配置文件 ├── requirements.txt # 项目依赖 ├── test_cases/ # 测试用例目录 │ ├── __init__.py │ ├── test_login.py │ └── test_search.py ├── page_objects/ # 页面对象目录 │ ├── __init__.py │ ├── base_page.py # 所有页面的基类 │ ├── login_page.py # 登录页面 │ └── home_page.py # 主页 └── utils/ # 工具类目录 ├── __init__.py ├── config_reader.py # 读取配置文件 └── logger.py # 日志记录base_page.py通常包含所有页面对象的通用方法,比如元素查找的封装、等待条件的封装、日志记录等:
# 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 import logging class BasePage: def __init__(self, driver): self.driver = driver self.logger = logging.getLogger(__name__) def find_element(self, by, locator, timeout=10): """查找单个元素,加入显式等待""" try: element = WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((by, locator)) ) self.logger.info(f"成功定位元素: {locator}") return element except TimeoutException: self.logger.error(f"定位元素超时: {locator}") raise def click_element(self, by, locator): """点击元素""" element = self.find_element(by, locator) element.click() self.logger.info(f"点击元素: {locator}") def input_text(self, by, locator, text): """向输入框输入文本""" element = self.find_element(by, locator) element.clear() element.send_keys(text) self.logger.info(f"向元素 {locator} 输入文本: {text}")login_page.py继承基类,定义登录页面特有的元素和操作:
# page_objects/login_page.py from selenium.webdriver.common.by import By from .base_page import BasePage class LoginPage(BasePage): # 页面元素定位器 USERNAME_INPUT = (By.ID, 'username') PASSWORD_INPUT = (By.ID, 'password') LOGIN_BUTTON = (By.ID, 'loginBtn') ERROR_MSG = (By.CLASS_NAME, 'error-message') def __init__(self, driver): super().__init__(driver) self.driver = driver def login(self, username, password): """登录操作""" self.input_text(*self.USERNAME_INPUT, username) self.input_text(*self.PASSWORD_INPUT, password) self.click_element(*self.LOGIN_BUTTON) def get_error_message(self): """获取错误提示信息""" try: return self.find_element(*self.ERROR_MSG).text except: return None最后,在测试用例中,你的代码将变得非常清晰和业务化:
# test_cases/test_login.py import pytest from page_objects.login_page import LoginPage from page_objects.home_page import HomePage class TestLogin: def test_login_success(self, driver): # 使用Fixture注入driver login_page = LoginPage(driver) login_page.driver.get("https://example.com/login") login_page.login("valid_user", "valid_pass") home_page = HomePage(driver) # 断言登录成功,例如检查是否跳转到首页,或出现欢迎语 assert home_page.is_welcome_message_displayed() @pytest.mark.parametrize("username, password, expected_error", [ ("", "password", "用户名不能为空"), ("user", "", "密码不能为空"), ("wrong", "wrong", "用户名或密码错误"), ]) def test_login_failure(self, driver, username, password, expected_error): login_page = LoginPage(driver) login_page.driver.get("https://example.com/login") login_page.login(username, password) # 断言出现了预期的错误信息 assert login_page.get_error_message() == expected_error注意事项:在实现PO模型时,一个常见的误区是把所有操作都塞进PO类,导致类变得臃肿。好的实践是:PO类只负责当前页面的元素定位和最基本的原子操作(如输入、点击)。业务流程(如“登录-搜索-下单”)应该在测试用例或用例层的业务封装模块里组合调用多个PO的方法。这保持了PO的纯粹性和可复用性。
3. 核心配置与环境搭建详解
3.1 依赖管理与虚拟环境
任何Python项目的第一步都是管理依赖。强烈建议使用虚拟环境(venv或conda)来隔离项目环境。在项目根目录下创建requirements.txt文件:
# requirements.txt selenium>=4.0.0 # 使用较新的4.x版本,API更现代 pytest>=7.0.0 pytest-html>=3.0.0 # 生成HTML报告 pytest-xdist>=3.0.0 # 并行测试 pytest-rerunfailures>=10.0 # 失败重试 allure-pytest>=2.9.0 # 生成Allure报告 webdriver-manager>=3.8.0 # 自动管理浏览器驱动,强烈推荐!使用webdriver-manager是一个革命性的最佳实践。它彻底解决了“手动下载、放置、匹配ChromeDriver版本”这一繁琐且易错的步骤。你不再需要去官网下载驱动,只需在代码中:
# conftest.py 中使用 webdriver-manager from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager @pytest.fixture(scope="function") def driver(): # 自动下载和管理合适版本的ChromeDriver service = Service(ChromeDriverManager().install()) options = webdriver.ChromeOptions() driver = webdriver.Chrome(service=service, options=options) yield driver driver.quit()webdriver-manager同样支持Firefox、Edge等。这保证了你的测试代码在任何新安装的环境下都能一键运行,极大提升了框架的移植性和团队协作效率。
3.2 配置文件与全局Fixture设计
测试框架需要适应不同环境(开发、测试、生产)和不同配置(浏览器类型、超时时间、基础URL)。我们将这些配置外置。
config.yaml(或config.ini):
# config/config.yaml base: url: "https://test.example.com" implicit_wait: 10 explicit_wait: 20 browser: name: "chrome" # chrome, firefox, edge headless: true # 是否无头模式运行 window_size: "1920,1080" report: html_report_path: "./reports/html" allure_report_path: "./reports/allure"conftest.py是这个框架的“中枢神经系统”,它存放会被多个测试模块共享的Fixture和钩子函数。
# conftest.py import pytest import yaml from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.firefox.service import Service as FirefoxService from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.firefox import GeckoDriverManager def load_config(): with open('config/config.yaml', 'r', encoding='utf-8') as f: return yaml.safe_load(f) @pytest.fixture(scope='session') def config(): """提供配置信息的会话级Fixture""" return load_config() @pytest.fixture(scope='function') def driver(config): """核心Fixture:根据配置创建和销毁WebDriver实例""" browser_name = config['browser']['name'].lower() driver = None if browser_name == 'chrome': options = webdriver.ChromeOptions() if config['browser']['headless']: options.add_argument('--headless=new') # Chrome 109+ 推荐使用new options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') options.add_argument(f'--window-size={config["browser"]["window_size"]}') service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options) elif browser_name == 'firefox': options = webdriver.FirefoxOptions() if config['browser']['headless']: options.add_argument('--headless') service = FirefoxService(GeckoDriverManager().install()) driver = webdriver.Firefox(service=service, options=options) else: raise ValueError(f"不支持的浏览器: {browser_name}") # 设置等待时间 driver.implicitly_wait(config['base']['implicit_wait']) driver.maximize_window() # 非headless模式下最大化窗口 yield driver # 测试结束后的清理工作 if driver: driver.quit() @pytest.fixture def base_url(config): """提供基础URL的Fixture""" return config['base']['url']这样设计后,你的测试用例只需要关心业务逻辑,环境细节全部由conftest.py和配置文件管理。
3.3 测试数据管理
测试数据不应硬编码在用例中。常见的管理方式有:
- JSON/YAML文件:适合结构化的静态数据。
- CSV/Excel文件:适合表格数据,易于用Excel编辑。
- 数据库:适合需要动态获取或验证的数据。
- Python数据类/字典:简单场景下直接在代码中定义。
建议根据数据类型和复杂度混合使用。例如,将用户登录凭证放在data/login_data.yaml中:
# data/login_data.yaml success: username: "standard_user" password: "secret_sauce" failure_cases: - username: "" password: "secret_sauce" expected_error: "Username is required" - username: "locked_out_user" password: "secret_sauce" expected_error: "Sorry, this user has been locked out."然后在测试中读取:
import yaml import pytest def load_login_data(): with open('data/login_data.yaml', 'r') as f: return yaml.safe_load(f) login_data = load_login_data() @pytest.mark.parametrize('case', login_data['failure_cases']) def test_login_failure(driver, case): # 使用case['username'], case['password'], case['expected_error'] pass4. 高级特性与实战技巧
4.1 等待机制:告别time.sleep的智慧
不稳定的自动化脚本,十有八九是等待没处理好。Selenium提供了三种等待:
- 强制等待:
time.sleep(n)。绝对禁止在正式框架中使用,它是脚本脆弱和低效的元凶。 - 隐式等待:
driver.implicitly_wait(10)。设置一个全局的超时时间,在查找任何元素时,如果元素没有立即出现,WebDriver会轮询查找直到超时。它是一把“钝器”,对某些需要更精细控制的场景(如等待元素可点击、消失)无能为力。 - 显式等待:
WebDriverWait配合expected_conditions。这是推荐的最佳实践。它允许你为某个特定的条件设置等待,条件满足则立即继续,超时则抛出异常。
在BasePage中,我们已经封装了基于显式等待的find_element方法。但实际场景更复杂:
场景一:等待元素可点击(而不仅仅是存在)
from selenium.webdriver.support.expected_conditions import element_to_be_clickable def wait_for_element_clickable(self, by, locator, timeout=10): wait = WebDriverWait(self.driver, timeout) element = wait.until(element_to_be_clickable((by, locator))) return element场景二:等待元素消失(如加载动画)
from selenium.webdriver.support.expected_conditions import invisibility_of_element_located def wait_for_element_invisible(self, by, locator, timeout=10): wait = WebDriverWait(self.driver, timeout) return wait.until(invisibility_of_element_located((by, locator)))场景三:自定义等待条件
# 等待页面标题包含特定文字 def wait_for_title_contains(self, text, timeout=10): wait = WebDriverWait(self.driver, timeout) return wait.until(lambda d: text in d.title) # 等待JavaScript返回特定值(例如,等待某个Vue/React组件加载完成) def wait_for_js_condition(self, js_script, expected_value, timeout=10): wait = WebDriverWait(self.driver, timeout) return wait.until(lambda d: d.execute_script(js_script) == expected_value)实操心得:很多现代Web应用(单页应用SPA)在操作后页面URL甚至不会改变。判断页面加载或操作完成的标志,不再是传统的
document.readyState,而是特定元素的状态。例如,点击“提交”后,等待“提交成功”的提示框出现,或者等待一个全局的“加载中”遮罩层消失。与开发团队约定好这些“可等待”的元素,是提高自动化稳定性的关键合作。
4.2 失败处理与截图、日志
测试失败时,光有一个断言错误堆栈是不够的。我们需要知道失败那一刻,浏览器里是什么样子。Pytest的钩子函数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 report = outcome.get_result() # 只关注测试用例本身的调用阶段(setup, call, teardown中的call) if report.when == "call" and report.failed: # 获取driver fixture(假设测试用例使用了名为driver的fixture) driver_fixture = item.funcargs.get('driver', None) if driver_fixture: take_screenshot(driver_fixture, report.nodeid) def take_screenshot(driver, nodeid): """截图并保存""" # 创建截图目录 screenshot_dir = "reports/screenshots" os.makedirs(screenshot_dir, exist_ok=True) # 生成文件名:用测试节点ID和时间戳 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # 将nodeid中的非法文件名字符替换掉 safe_nodeid = nodeid.replace("::", "_").replace("/", "_").replace("\\", "_").replace(".", "_") filename = f"{safe_nodeid}_{timestamp}.png" filepath = os.path.join(screenshot_dir, filename) driver.save_screenshot(filepath) print(f"\n测试失败截图已保存至: {filepath}")同时,集成Python标准库的logging模块,为框架添加日志记录,便于追踪执行流程和调试。
# utils/logger.py import logging import os from datetime import datetime def setup_logger(name=__name__, log_level=logging.INFO): """配置并返回一个logger实例""" logger = logging.getLogger(name) logger.setLevel(log_level) # 避免重复添加handler if not logger.handlers: # 控制台Handler console_handler = logging.StreamHandler() console_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') console_handler.setFormatter(console_format) logger.addHandler(console_handler) # 文件Handler log_dir = "logs" os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, f"automation_{datetime.now().strftime('%Y%m%d')}.log") file_handler = logging.FileHandler(log_file, encoding='utf-8') file_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') file_handler.setFormatter(file_format) logger.addHandler(file_handler) return logger # 在BasePage中使用 # self.logger = setup_logger(__name__) # self.logger.info("打开页面: %s", url)4.3 测试报告的艺术:从HTML到Allure
清晰的测试报告是自动化测试价值的直观体现。Pytest原生支持多种报告格式,但最常用的是通过插件生成。
1. 生成简单的HTML报告:安装pytest-html后,运行测试时添加参数即可:
pytest --html=reports/report.html --self-contained-html--self-contained-html参数会将CSS和JS嵌入到单个HTML文件中,方便分享。这份报告包含了用例通过率、执行时长和简单的失败摘要。
2. 生成强大的Allure报告:Allure报告提供了仪表盘、图表、用例分组、附件(截图、日志)等高级功能,是团队汇报和问题分析的利器。
- 安装:
pip install allure-pytest - 运行测试,生成Allure结果数据:
pytest --alluredir=./reports/allure-results - 生成并打开HTML报告(需要先安装Allure命令行工具):
allure generate ./reports/allure-results -o ./reports/allure-report --clean allure open ./reports/allure-report
为了让Allure报告更丰富,我们可以在测试用例和Fixture中添加装饰器和附件:
import allure import pytest @pytest.fixture(scope='function') def driver(config): ... # 初始化driver yield driver # 测试结束后,如果失败,将截图和页面源码附加到Allure报告 if hasattr(driver, '_test_failed') and driver._test_failed: allure.attach(driver.get_screenshot_as_png(), name="失败截图", attachment_type=allure.attachment_type.PNG) allure.attach(driver.page_source, name="页面源码", attachment_type=allure.attachment_type.HTML) driver.quit() @allure.feature("登录模块") @allure.story("用户登录功能") class TestLogin: @allure.title("使用有效凭证登录成功") @allure.severity(allure.severity_level.CRITICAL) def test_login_success(self, driver): with allure.step("打开登录页面"): driver.get("https://example.com/login") with allure.step("输入用户名和密码"): # ... 操作 with allure.step("点击登录按钮"): # ... 操作 with allure.step("验证登录成功"): assert "Dashboard" in driver.title生成的Allure报告会按照Feature、Story、Step层级展示,非常清晰。
4.4 并行测试与失败重试
当用例数量成百上千时,串行执行会非常耗时。pytest-xdist插件可以实现测试的分布式执行。
# 使用2个worker并行执行 pytest -n 2 # 自动检测CPU核心数 pytest -n auto注意事项:并行测试时,必须确保测试用例之间是独立的,没有共享状态(如共享的浏览器实例、共享的测试数据)。我们的
driverFixture的scope设置为function,并且没有使用@pytest.fixture(scope='session')来共享一个浏览器,这天然支持了并行。如果测试依赖特定的数据库状态或文件,则需要更复杂的 setup/teardown 逻辑或使用独立的测试环境。
网络不稳定或测试环境偶发问题可能导致用例“假失败”。pytest-rerunfailures插件可以自动重试失败的用例。
# 对失败用例重试最多2次,每次间隔1秒 pytest --reruns 2 --reruns-delay 1这个功能可以显著提高测试套件的稳定性,但需谨慎使用。对于确实的bug导致的失败,重试只会掩盖问题。通常建议只在CI/CD流水线中针对不稳定的测试子集使用重试策略。
5. 完整实战:从零搭建一个登录测试套件
让我们把上面的所有部分组合起来,完成一个完整的、可运行的登录测试套件。
项目结构最终版:
selenium_pytest_framework_demo/ ├── config/ │ └── config.yaml ├── data/ │ └── login_data.yaml ├── logs/ (自动生成) ├── page_objects/ │ ├── __init__.py │ ├── base_page.py │ └── login_page.py ├── reports/ (自动生成) │ ├── allure-report/ │ ├── allure-results/ │ ├── html/ │ └── screenshots/ ├── test_cases/ │ ├── __init__.py │ └── test_login.py ├── utils/ │ ├── __init__.py │ └── logger.py ├── conftest.py ├── pytest.ini └── requirements.txtpytest.ini配置文件:
[pytest] # 指定测试文件的位置和命名规则 testpaths = test_cases python_files = test_*.py python_classes = Test* python_functions = test_* # 添加命令行默认选项 addopts = -v # 详细输出 --strict-markers # 严格检查marker --tb=short # 失败时输出简短的traceback --html=reports/html/report.html --self-contained-html --alluredir=reports/allure-results # 自定义标记,用于分类测试 markers = smoke: 冒烟测试用例 regression: 回归测试用例 slow: 执行较慢的测试最终的执行与集成:
本地运行所有测试:
pip install -r requirements.txt pytest # 或指定标记运行冒烟测试 pytest -m smoke在CI/CD中运行(例如GitHub Actions):
# .github/workflows/test.yml name: UI Automation Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install dependencies run: | pip install -r requirements.txt - name: Install Chrome and ChromeDriver run: | sudo apt-get update sudo apt-get install -y google-chrome-stable - name: Run tests with pytest run: | pytest --headless -v - name: Upload HTML report uses: actions/upload-artifact@v3 if: always() with: name: html-report path: reports/html/
6. 常见问题排查与性能优化
即使框架搭建得再完善,在实际运行中还是会遇到各种问题。这里记录一些高频问题的排查思路。
问题1:元素定位不到,报NoSuchElementException
- 可能原因1:动态ID或类名。现代前端框架(如React, Vue)经常生成随机的属性值。解决方案:与前端开发沟通,为关键测试元素添加固定的
>prefs = {"profile.managed_default_content_settings.images": 2} options.add_experimental_option("prefs", prefs) - 优化点4:使用更快的定位策略。通常
By.ID和By.CSS_SELECTOR比By.XPATH快,尤其是复杂的XPath表达式。
问题4:Selenium被网站检测到
- 一些反爬机制严格的网站能检测到Selenium的自动化特征。缓解措施(非万能):
- 使用
undetected-chromedriver库(一个修改版的ChromeDriver)。 - 添加
excludeSwitches选项:options.add_experimental_option("excludeSwitches", ["enable-automation"])。 - 添加
useAutomationExtension选项:options.add_experimental_option('useAutomationExtension', False)。 - 修改
navigator.webdriver属性(需通过CDP协议):driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': 'Object.defineProperty(navigator, \"webdriver\", {get: () => undefined})'})。
重要提示:这些方法可能随时失效,且用于绕过公开网站的自动化检测可能违反其服务条款。自动化测试应主要针对自己公司可控的测试环境或产品。
- 使用
从一堆零散的、充斥着time.sleep的Selenium脚本,到一个结构清晰、配置灵活、报告完善、易于维护和集成的Selenium+Pytest自动化测试框架,这个转变带来的收益是巨大的。它不仅仅是代码组织方式的变化,更是测试思维从“实现功能”到“构建工程”的升级。这个框架就像为你打造了一个坚固的测试流水线,你只需要往里添加具体的业务测试用例(Page Objects和Test Cases),它就能稳定、高效、可视化的运行,并持续为你提供高质量的反馈。