1. 项目概述:为什么选择这个技术栈?
如果你刚接触自动化测试,或者想从零开始搭建一个稳定、可维护的测试环境,那么“Python + Pytest + Selenium”这个组合,绝对是你的不二之选。我从业十多年,带过不少团队,也踩过无数环境的坑,最终沉淀下来的就是这个黄金三角。它不是什么高深莫测的黑科技,但胜在简单、强大、生态好。Python的语法接近自然语言,上手快;Pytest是目前Python测试领域事实上的标准,写用例就像写文档一样清晰;Selenium则是浏览器自动化的老牌王者,能模拟几乎所有用户操作。把它们仨组合起来,你就能快速构建一个从元素定位到测试报告生成的全流程自动化框架。今天,我就带你从零开始,一步步搭建这个环境,过程中我会分享那些官方文档里不会写的“坑”和“技巧”,确保你一次成功,少走弯路。
2. 环境准备与核心工具安装
搭建环境就像盖房子打地基,基础不牢,地动山摇。很多人失败就失败在第一步——环境没配好。我们按顺序来,确保每一步都清晰无误。
2.1 Python安装与配置:避开第一个大坑
Python是这一切的基石。首先,去Python官网下载安装包。这里有个关键选择:强烈建议选择Python 3.8至3.11之间的版本。版本太老(如3.6)可能缺少某些新库的支持,版本太新(如3.12+)有时会遇到第三方库兼容性问题,作为生产环境,稳定压倒一切。
下载完成后运行安装程序。这里有一个99%的新手都会忽略,但会导致后续无数报错的细节:务必勾选“Add Python X.X to PATH”这个选项。它的作用是把Python和它的脚本工具目录添加到系统的环境变量里。如果不勾选,你就得手动去配置,对于新手来说极易出错。
安装完成后,我们需要验证一下。打开命令行(Windows下按Win+R,输入cmd;Mac或Linux打开终端),输入以下命令:
python --version如果正确显示Python版本号(如Python 3.10.11),恭喜你,第一步成功了。如果显示“不是内部或外部命令”,那就说明环境变量没配好。你需要手动添加:右键“此电脑”->“属性”->“高级系统设置”->“环境变量”,在“系统变量”里找到Path,编辑,新建两条,分别指向你的Python安装目录(例如C:\Users\YourName\AppData\Local\Programs\Python\Python310)和它的Scripts目录(例如C:\Users\YourName\AppData\Local\Programs\Python\Python310\Scripts)。
注意:很多教程会教你用
pip install来装包,但pip这个命令本身就在Scripts目录下。如果没配好环境变量,你连pip都用不了,后续所有安装都无法进行。所以,这一步是重中之重。
2.2 使用虚拟环境:项目隔离的必修课
直接在本机Python环境里安装所有包是极其不推荐的。想象一下,你同时做A、B两个项目,A项目需要Selenium 4.10,B项目需要Selenium 4.15,全局安装只能有一个版本,必然冲突。虚拟环境(Virtual Environment)就是为每个项目创建一个独立的Python运行沙盒,互不干扰。
创建虚拟环境非常简单。在你项目的根目录下(比如你新建一个auto_test_project文件夹),打开命令行,执行:
python -m venv venv这个命令会在当前目录下创建一个名为venv的文件夹,里面包含了一个独立的Python解释器和pip。接下来,激活这个环境:
- Windows:
venv\Scripts\activate - Mac/Linux:
source venv/bin/activate
激活后,你的命令行提示符前面会显示(venv),表示你已经进入了这个虚拟环境。之后所有pip install的操作,都只会影响这个环境。
实操心得:我习惯把
venv文件夹添加到.gitignore文件中,不提交到代码仓库。每个开发者拉取代码后,自己创建并激活虚拟环境,再根据requirements.txt安装依赖,这样可以保证团队环境一致。
2.3 安装核心三件套:Pytest, Selenium, WebDriver
环境激活后,我们就可以安装核心工具了。这里不建议一个个安装,而是通过一个命令安装我们所需的基础套件。在激活的虚拟环境命令行中,执行:
pip install pytest selenium pytest-html allure-pytest我来解释一下每个包的作用:
- pytest: 测试框架本体,用于组织、发现和运行测试用例。
- selenium: 浏览器自动化库,提供了用代码控制浏览器的API。
- pytest-html: 一个Pytest插件,用于生成简洁的HTML格式测试报告。
- allure-pytest: 另一个强大的报告插件,能生成非常美观、详细的Allure报告(可选,但推荐)。
安装过程如果遇到网络慢或超时,可以临时使用国内镜像源加速,例如:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pytest selenium安装完成后,可以验证一下:
pytest --version python -c "import selenium; print(selenium.__version__)"能正常输出版本号即可。
最后,也是最关键的一步:下载浏览器驱动。Selenium本身只是一个“指挥者”,它需要对应的“驾驶员”(WebDriver)来实际操控浏览器。以最常用的Chrome浏览器为例:
- 查看你电脑上Chrome的版本(在浏览器地址栏输入
chrome://settings/help)。 - 打开ChromeDriver官网或国内镜像站,下载与你Chrome版本号完全一致的驱动。
- 将下载的
chromedriver.exe(Windows)文件,放到一个你记得住的目录,比如C:\WebDriver,并将这个目录添加到系统的Path环境变量中。另一种更常见的做法是,直接把chromedriver.exe放在你项目的根目录下,然后在代码中指定它的路径。我推荐后者,因为更利于项目移植。
避坑指南:浏览器驱动版本必须与浏览器版本匹配!这是Selenium新手报错的重灾区。常见的错误信息是“This version of ChromeDriver only supports Chrome version XX”。如果找不到完全一致的版本,可以尝试下载版本号最接近的。或者,使用
webdriver-manager这个库,它可以自动下载和管理匹配的驱动,省去手动操作的麻烦。安装命令是pip install webdriver-manager,在代码中这样使用:from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
3. 项目结构与第一个自动化脚本
工具齐备,现在我们来搭建一个清晰、可扩展的项目结构。混乱的目录是项目后期难以维护的罪魁祸首。
3.1 构建清晰的项目目录
一个好的目录结构能让你和你的团队事半功倍。我推荐如下结构:
auto_test_project/ ├── drivers/ # 存放浏览器驱动(如果不用webdriver-manager) ├── test_cases/ # 存放所有的测试用例文件 │ ├── __init__.py │ └── test_login.py # 示例测试模块 ├── page_objects/ # 页面对象模型(PO模式)目录 │ ├── __init__.py │ └── login_page.py # 示例页面类 ├── common/ # 公共模块 │ ├── __init__.py │ ├── base_page.py # 页面基类 │ └── logger.py # 日志模块 ├── reports/ # 测试报告输出目录 ├── conftest.py # Pytest的共享夹具配置 ├── pytest.ini # Pytest配置文件 └── requirements.txt # 项目依赖列表你可以使用tree命令(需要安装)或在IDE中手动创建这些文件夹和文件。其中,__init__.py文件的作用是让Python将这个目录视为一个包(Package),从而可以导入其中的模块。
3.2 编写第一个Selenium测试用例
让我们从一个最简单的例子开始,感受一下自动化测试的魔力。在test_cases目录下,创建文件test_baidu_search.py。
import pytest from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys import time class TestBaiduSearch: """一个简单的百度搜索测试用例""" def setup_method(self): """每个测试方法开始前执行:初始化浏览器""" # 初始化Chrome浏览器驱动 self.driver = webdriver.Chrome() # 窗口最大化 self.driver.maximize_window() # 设置隐式等待10秒。意思是查找元素时,如果立即没找到,会等待最多10秒,期间不断重试。 self.driver.implicitly_wait(10) def teardown_method(self): """每个测试方法结束后执行:关闭浏览器""" # 等待3秒,方便肉眼观察结果 time.sleep(3) # 关闭浏览器并退出驱动 self.driver.quit() def test_search_selenium(self): """测试搜索关键字‘Selenium’""" # 1. 打开百度首页 self.driver.get("https://www.baidu.com") # 2. 定位搜索输入框。By.ID 表示通过HTML元素的id属性定位,'kw'是百度搜索框的id。 search_box = self.driver.find_element(By.ID, 'kw') # 3. 在搜索框中输入文本“Selenium” search_box.send_keys("Selenium") # 4. 模拟键盘按下回车键进行搜索 search_box.send_keys(Keys.RETURN) # 5. 断言:验证搜索结果页面标题中包含“Selenium”这个词 assert "Selenium" in self.driver.title def test_search_pytest(self): """测试搜索关键字‘Pytest’""" self.driver.get("https://www.baidu.com") search_box = self.driver.find_element(By.ID, 'kw') search_box.send_keys("Pytest") search_box.send_keys(Keys.RETURN) assert "Pytest" in self.driver.title这个脚本定义了一个测试类TestBaiduSearch。setup_method和teardown_method是Pytest的夹具(Fixtures)的一种用法,分别在每个测试方法(test_开头)执行前后运行,用于初始化和清理。在测试方法里,我们完成了“打开浏览器->访问网页->定位元素->操作元素->断言结果”的标准流程。
3.3 使用Pytest运行并查看结果
保存文件后,在项目根目录(auto_test_project)下打开命令行(确保虚拟环境已激活),直接运行:
pytest test_cases/test_baidu_search.py -v-v参数表示输出详细信息。你会看到Pytest开始执行,自动打开Chrome浏览器,完成搜索操作,然后关闭。命令行会输出类似以下的结果:
============================= test session starts ============================= platform win32 -- Python 3.10.11, pytest-7.4.0, pluggy-1.2.0 rootdir: C:\auto_test_project collected 2 items test_cases/test_baidu_search.py::TestBaiduSearch::test_search_selenium PASSED test_cases/test_baidu_search.py::TestBaiduSearch::test_search_pytest PASSED ============================== 2 passed in 15.23s =============================两个测试用例都通过了!这就是你的第一个自动化测试。但现在的代码还很初级,浏览器在每个用例都重启一次,效率低,且元素定位、业务逻辑和测试逻辑混杂在一起,难以维护。接下来,我们要引入更高级的模式和配置。
4. 进阶配置与最佳实践
要让这个框架真正用于项目,我们需要解决效率、可维护性和报告可视化的问题。
4.1 使用Conftest.py共享夹具
上面的例子中,每个测试类都要自己写setup_method和teardown_method,很冗余。Pytest的conftest.py文件可以定义全局共享的夹具。在项目根目录创建conftest.py文件:
import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options @pytest.fixture(scope="class") def driver_init(request): """ 一个类级别的夹具,为每个测试类提供唯一的driver实例。 scope="class": 表示这个夹具在每个测试类中只执行一次初始化,类中的所有测试方法共享同一个driver。 request: pytest内置参数,可以访问请求该夹具的测试上下文。 """ # Chrome选项配置 chrome_options = Options() # 无头模式:不显示浏览器GUI,在后台运行,适合CI/CD环境。 # chrome_options.add_argument("--headless") # 禁用GPU加速,在某些环境下可避免问题 chrome_options.add_argument("--disable-gpu") # 禁用沙箱,在Docker或某些Linux系统上可能需要 chrome_options.add_argument("--no-sandbox") # 禁用DevShmUsage,解决Linux下内存不足问题 chrome_options.add_argument("--disable-dev-shm-usage") # 初始化驱动,并传入选项 driver = webdriver.Chrome(options=chrome_options) driver.maximize_window() driver.implicitly_wait(10) # 将driver实例赋值给测试类的 `driver` 属性,这样测试类中就能用 `self.driver` 访问了。 request.cls.driver = driver # yield 之前是setup部分,之后是teardown部分。yield将driver对象传递给测试函数。 yield # 所有测试执行完毕后,关闭浏览器 driver.quit()然后,修改我们的测试用例文件,使用这个共享夹具:
import pytest from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys @pytest.mark.usefixtures("driver_init") # 使用名为"driver_init"的夹具 class TestBaiduSearchAdvanced: """使用共享夹具的进阶测试用例""" def test_search_selenium(self): """测试搜索关键字‘Selenium’""" self.driver.get("https://www.baidu.com") search_box = self.driver.find_element(By.ID, 'kw') search_box.send_keys("Selenium" + Keys.RETURN) assert "Selenium" in self.driver.title def test_search_pytest(self): """测试搜索关键字‘Pytest’""" self.driver.get("https://www.baidu.com") search_box = self.driver.find_element(By.ID, 'kw') search_box.send_keys("Pytest" + Keys.RETURN) assert "Pytest" in self.driver.title现在,测试类简洁多了。@pytest.mark.usefixtures("driver_init")这个装饰器告诉Pytest,这个测试类要使用conftest.py中定义的driver_init夹具。scope="class"保证了同一个类里的多个测试方法共用同一个浏览器会话,大大提升了执行速度。
4.2 引入页面对象模型
直接在测试用例里写find_element和send_keys是“脚本式”的写法,一旦页面元素ID变了,你需要修改所有用到它的测试用例。页面对象模型是解决这个问题的标准设计模式。它的核心思想是:将页面的元素定位和操作封装成一个类,测试用例只调用这个类的方法。
首先,在page_objects目录下创建baidu_page.py:
from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys class BaiduPage: """百度首页的页面对象类""" # 将页面元素定位器集中管理,像配置一样 URL = 'https://www.baidu.com' SEARCH_INPUT = (By.ID, 'kw') # 搜索框 SEARCH_BUTTON = (By.ID, 'su') # “百度一下”按钮 def __init__(self, driver): """初始化时需要传入driver对象""" self.driver = driver def load(self): """打开百度首页""" self.driver.get(self.URL) def search(self, keyword): """ 执行搜索操作 :param keyword: 搜索关键词 """ # 定位元素并操作 search_box = self.driver.find_element(*self.SEARCH_INPUT) # *用于解包元组 search_box.clear() # 先清空输入框,是个好习惯 search_box.send_keys(keyword) # 这里可以点击按钮,也可以按回车。我们按回车。 search_box.send_keys(Keys.RETURN) # 返回当前页面对象,支持链式调用(可选) return self然后,修改测试用例,使用页面对象:
import pytest from page_objects.baidu_page import BaiduPage @pytest.mark.usefixtures("driver_init") class TestBaiduSearchWithPO: """使用页面对象模型的测试用例""" def test_search_selenium(self): """使用PO模式搜索Selenium""" baidu_page = BaiduPage(self.driver) baidu_page.load().search("Selenium") assert "Selenium" in self.driver.title def test_search_pytest(self): """使用PO模式搜索Pytest""" baidu_page = BaiduPage(self.driver) baidu_page.load().search("Pytest") assert "Pytest" in self.driver.title看,测试用例变得多么清晰!它只关心业务逻辑(打开百度,搜索某个词),而不关心具体怎么找到输入框、怎么点击。如果百度搜索框的ID明天从kw变成了searchInput,你只需要去修改BaiduPage类中的SEARCH_INPUT这一个地方,所有测试用例都无需改动。这就是PO模式带来的高可维护性。
4.3 生成漂亮的测试报告
自动化测试不能光看命令行输出,我们需要直观的报告来展示测试结果。这里介绍两种最常用的报告插件。
1. 生成HTML报告:运行测试时,添加--html参数:
pytest test_cases/ --html=reports/report.html --self-contained-html--self-contained-html参数会将CSS样式内嵌到HTML文件中,生成一个独立的报告文件。打开reports/report.html,你就能看到一个包含通过率、执行时间、错误详情等信息的网页报告。
2. 生成Allure报告(更推荐):Allure报告非常强大和美观。首先确保安装了allure-pytest。运行测试时,需要指定一个目录来存放原始的Allure结果数据:
pytest test_cases/ --alluredir=reports/allure-results运行完成后,数据已经生成,但需要Allure命令行工具来渲染成HTML报告。你需要先去Allure官网下载命令行工具,并配置到系统Path。然后执行:
allure generate reports/allure-results -o reports/allure-report --clean allure open reports/allure-report最后一条命令会自动在浏览器中打开生成的Allure报告。报告里会有清晰的用例分类、步骤详情、截图(需要额外配置)、历史趋势图等,非常专业。
实操心得:我通常会在
conftest.py中配置自动截图,当测试失败时,自动截取当前页面并附加到Allure报告中。这能极大方便错误定位。配置代码如下:import allure from datetime import datetime @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() if rep.when == "call" and rep.failed: # 只有测试执行失败时才截图 driver = item.cls.driver if hasattr(item.cls, 'driver') else None if driver: # 生成带时间戳的截图文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") screenshot_name = f"screenshot_{item.name}_{timestamp}.png" screenshot_path = f"./reports/screenshots/{screenshot_name}" driver.save_screenshot(screenshot_path) # 将截图附加到Allure报告 allure.attach.file(screenshot_path, name="失败截图", attachment_type=allure.attachment_type.PNG)
5. 常见问题排查与性能优化
即使环境搭好了,在编写和运行脚本时,你依然会遇到各种各样的问题。这里我总结了一些高频问题和优化技巧。
5.1 元素定位失败问题大全
这是Selenium自动化中最常见的问题,没有之一。错误信息通常是NoSuchElementException。
原因1:页面未加载完成就进行定位。
- 解决方案:使用显式等待。这是比隐式等待更精确、更推荐的方式。它允许你为某个特定元素设置等待条件。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 等待最多10秒,直到ID为‘kw’的元素出现在DOM中并可见 wait = WebDriverWait(driver, 10) search_box = wait.until(EC.visibility_of_element_located((By.ID, 'kw'))) search_box.send_keys("hello")expected_conditions模块提供了很多条件,如元素可点击、元素存在、标题包含某文字等。
原因2:元素在iframe或shadow DOM内部。
- 解决方案:需要先切换到对应的iframe或shadow root。
# 切换到iframe iframe = driver.find_element(By.TAG_NAME, 'iframe') driver.switch_to.frame(iframe) # 操作iframe内的元素... # 操作完后切回主文档 driver.switch_to.default_content()原因3:元素是动态生成的,ID或Class每次都会变。
- 解决方案:使用相对定位方式,如XPath或CSS Selector,通过其文本内容、属性组合或层级关系来定位。
# 通过部分文本定位链接 driver.find_element(By.XPATH, "//a[contains(text(), '登录')]") # 通过属性组合定位 driver.find_element(By.CSS_SELECTOR, "input[name='username'][type='text']")注意:XPath尽量少用绝对路径(以
/开头),多用相对路径(以//开头),并避免使用索引(如div[1]),因为页面结构一变就容易失效。
原因4:页面有多个相同特征的元素,定位到了第一个但不是你想要的那个。
- 解决方案:使用
find_elements(复数)获取列表,然后按索引或进一步筛选。
buttons = driver.find_elements(By.CLASS_NAME, 'submit-btn') if len(buttons) > 1: buttons[1].click() # 点击第二个按钮5.2 测试执行稳定性与速度优化
1. 使用无头模式:在conftest.py的Chrome选项中取消注释--headless参数,浏览器将在后台运行,不显示GUI,节省资源且速度更快,特别适合在服务器或持续集成环境中运行。
2. 优化等待策略:
- 减少/避免使用
time.sleep():这是固定等待,效率最低。尽量用显式等待代替。 - 合理设置隐式等待时间:一般10秒足够,太长会拖慢失败用例的执行速度。
- 混合使用显式与隐式等待:Pytest官方不推荐同时用,容易导致不可预知的超时。建议只使用显式等待,并封装成通用方法。
3. 使用PageFactory或BasePage封装通用操作:创建一个BasePage类,封装所有页面的通用操作,如等待元素、点击、输入、截图等。让具体的页面类(如BaiduPage)继承它。
# common/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.wait = WebDriverWait(driver, 10) def find_element(self, by, locator): """查找单个元素,并等待其可见""" return self.wait.until(EC.visibility_of_element_located((by, locator))) def click(self, by, locator): """点击元素""" self.find_element(by, locator).click() def input_text(self, by, locator, text): """向元素输入文本""" element = self.find_element(by, locator) element.clear() element.send_keys(text)这样,在具体的页面类中,操作就变得更简洁、更健壮。
5.3 配置管理与数据驱动
1. 使用pytest.ini进行配置:在项目根目录创建pytest.ini文件,可以统一管理Pytest的默认行为。
[pytest] # 自动发现测试文件的规则 testpaths = test_cases # 匹配测试文件名的模式 python_files = test_*.py # 匹配测试类名的模式 python_classes = Test* # 匹配测试方法名的模式 python_functions = test_* # 命令行默认参数 addopts = -v --html=reports/report.html --self-contained-html --alluredir=reports/allure-results # 标记过滤器(例如只运行标记为‘smoke’的用例) # markers = # smoke: 冒烟测试配置好后,在项目根目录直接运行pytest命令,就会自动应用这些配置。
2. 使用数据驱动测试:同一个测试逻辑,用多组数据来验证。Pytest的@pytest.mark.parametrize装饰器非常好用。
import pytest @pytest.mark.parametrize("search_keyword, expected_title_part", [ ("Selenium", "Selenium"), ("Pytest", "Pytest"), ("Python", "Python"), ]) def test_baidu_search_with_data(driver_init, search_keyword, expected_title_part): """数据驱动测试示例""" driver = driver_init driver.get("https://www.baidu.com") driver.find_element(By.ID, 'kw').send_keys(search_keyword + Keys.RETURN) assert expected_title_part in driver.title这样,你写一个测试函数,就能运行三条测试用例,分别验证不同的搜索词。测试数据和测试逻辑分离,维护起来非常方便。
6. 将项目融入持续集成流程
个人学习或小团队使用,在本地运行可能就够了。但对于稍正式的项目,你需要把它接入持续集成/持续部署流水线,实现代码提交后自动触发测试。这里以最流行的GitHub Actions为例,给出一个最简单的配置。
在项目根目录创建.github/workflows/ci.yml文件:
name: Python Selenium CI on: [push, pull_request] # 在代码推送或拉取请求时触发 jobs: test: runs-on: ubuntu-latest # 使用最新的Ubuntu系统作为运行环境 steps: - name: Checkout code uses: actions/checkout@v3 # 步骤1:检出代码 - name: Set up Python uses: actions/setup-python@v4 # 步骤2:设置Python环境 with: python-version: '3.10' - name: Install dependencies run: | # 步骤3:安装依赖 pip install -r requirements.txt pip install pytest selenium pytest-html allure-pytest webdriver-manager - name: Install Chrome and ChromeDriver run: | # 步骤4:在Ubuntu系统上安装Chrome浏览器 sudo apt-get update sudo apt-get install -y google-chrome-stable - name: Run tests with Pytest run: | # 步骤5:运行测试,使用无头模式 # 设置一个环境变量,告诉Chrome在无头模式下运行 export PYTEST_ADDOPTS="--tb=short" # 运行测试并生成报告 pytest test_cases/ --html=reports/report.html --self-contained-html - name: Upload test report uses: actions/upload-artifact@v3 # 步骤6:上传生成的HTML报告作为工件 if: always() # 即使测试失败也上传报告 with: name: pytest-html-report path: reports/report.html这个工作流定义了:每当有代码推送到仓库,GitHub Actions就会启动一个Ubuntu虚拟机,自动安装Python、项目依赖、Chrome浏览器,然后以无头模式运行你的所有Pytest测试用例,最后将HTML报告保存起来供你下载查看。这样,自动化测试就真正成为了你开发流程中不可或缺的一环。
走到这里,你已经从一个零基础的新手,成功搭建了一个具备工程化雏形的Python+Pytest+Selenium自动化测试环境。这个环境包含了虚拟隔离、页面对象模型、共享夹具、多种报告生成、常见问题解决方案以及CI/CD集成思路。记住,框架是死的,人是活的。在实际项目中,你还需要根据业务特点,不断封装更多通用组件(如数据库操作、API调用、数据工厂等),并建立完善的测试数据管理和用例组织规范。