Python自动化测试框架实战:从0搭建到持续集成的完整攻略
先给你看看最终效果:原来跑100个用例要15分钟,现在并行执行只要3分钟;原来报错就一行”Element not found”,现在截图+日志+视频回放一步到位。废话不多说,直接上干货。
框架选型:为什么选了pytest+selenium+Allure?
市面上框架很多,我试过unittest、robotframework、甚至自己手写。结论是:unittest太死板,robotframework太重,手写纯粹找虐。
pytest的优势在于:
- 插件生态巨丰富(pytest-xdist并行、pytest-html报告)
- fixture机制比setUp/tearDown灵活10倍
- 参数化测试一行搞定
Selenium是Web自动化的标配,没争议。Allure就是那个能让报告看起来高大上的东西,领导看到都夸专业。
开局一张图:pytest负责调度,selenium负责操作,Allure负责报告,Jenkins负责自动化运行
项目结构:别再所有代码放一个文件了
我见过最离谱的代码:一个test.py文件800行,用例和数据混在一起,改个URL要把整个文件翻三遍。正确姿势是这样的:
“`python
auto_test_framework/
├── conftest.py # 全局fixture配置
├── pytest.ini # pytest配置文件
├── requirements.txt # 依赖管理
├── test_cases/ # 测试用例
│ ├── test_login.py
│ └── test_search.py
├── pages/ # Page Object模式
│ ├── login_page.py
│ └── search_page.py
├── data/ # 测试数据
│ ├── test_data.json
│ └── config.yaml
├── utils/ # 工具类
│ ├── driver.py # 浏览器驱动管理
│ ├── logger.py # 日志模块
│ └── screenshot.py # 截图模块
└── reports/ # 测试报告
“`
这个结构的核心思想:用例只关心业务流程,数据单独管理,页面元素封装成对象。这样改一个登录按钮的xpath,你只需要改pages/login_page.py,不用翻整个test_cases目录。
核心代码:写一个不会崩的浏览器驱动管理
第一个坑:每次跑完用例浏览器不关,跑100个用例开了100个浏览器窗口,内存直接爆炸。解决方案是写一个驱动管理器,配合pytest的fixture自动清理。
为什么要这么写:因为手动管理浏览器驱动太容易出幺蛾子,网络波动、浏览器版本更新都会导致驱动失效。
来看具体代码:
“`python
# utils/driver.py
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import os
class DriverManager:
“””浏览器驱动管理器,自动处理驱动版本和生命周期”””
def __init__(self):
self.driver = None
def get_driver(self):
“””获取WebDriver实例,支持远程和本地两种模式”””
if os.getenv(‘REMOTE_DRIVER’, ‘False’) == ‘True’:
# 远程模式,适用于Docker或Selenium Grid
chrome_options = Options()
chrome_options.add_argument(‘–headless’)
chrome_options.add_argument(‘–no-sandbox’)
chrome_options.add_argument(‘–disable-dev-shm-usage’)
driver = webdriver.Remote(
command_executor=’http://selenium-hub:4444/wd/hub’,
options=chrome_options
)
else:
# 本地模式,自动管理驱动版本
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
driver.implicitly_wait(10) # 隐式等待,避免元素加载慢导致报错
driver.maximize_window()
return driver
def quit_driver(self, driver):
“””安全关闭驱动”””
if driver:
driver.quit()
“`
然后在conftest.py中注册fixture:
“`python
# conftest.py
import pytest
from utils.driver import DriverManager
@pytest.fixture(scope=”function”)
def driver():
“””每个测试用例都使用独立的浏览器实例”””
manager = DriverManager()
web_driver = manager.get_driver()
yield web_driver
manager.quit_driver(web_driver)
“`
这个设计真的反人类?不,这个设计救了无数人的命。每个用例独立浏览器,一个用例失败不会影响下一个;fixture自动清理,再也不用担心内存泄漏。
数据驱动:告别硬编码,一个JSON跑遍所有场景
另一个坑:测试数据硬编码在用例里,换个环境就得改代码。更离谱的是,测试登录功能时,10个用例写了10个不同的用户名密码,改密码时改到崩溃。
为什么要这么写:把数据从代码中抽离,实现”用例与数据分离”,这样测试人员和开发可以并行工作。
“`python
# data/test_data.json
{
“login”: {
“valid_user”: {
“username”: “admin”,
“password”: “123456”,
“expected”: “登录成功”
},
“invalid_user”: {
“username”: “wrong”,
“password”: “wrong”,
“expected”: “用户名或密码错误”
},
“empty_fields”: {
“username”: “”,
“password”: “”,
“expected”: “请输入用户名”
}
},
“search”: {
“valid_keyword”: {
“keyword”: “python”,
“expected”: “搜索结果”
},
“empty_keyword”: {
“keyword”: “”,
“expected”: “请输入搜索关键词”
}
}
}
“`
然后在测试用例里用pytest的参数化功能,一行代码搞定所有场景:
“`python
# test_cases/test_login.py
import pytest
import json
from pages.login_page import LoginPage
def load_test_data():
with open(‘data/test_data.json’, ‘r’, encoding=’utf-8′) as f:
data = json.load(f)
return data[‘login’]
class TestLogin:
@pytest.mark.parametrize(“test_data”, load_test_data().values(), ids=load_test_data().keys())
def test_login_scenario(self, driver, test_data):
login_page = LoginPage(driver)
actual_result = login_page.login(test_data[‘username’], test_data[‘password’])
assert actual_result == test_data[‘expected’], \
f”预期结果:{test_data[‘expected’]},实际结果:{actual_result}”
“`
这样改密码只需要改JSON文件,测试代码一行都不用动。而且新增场景也简单,JSON里加一条数据就行,测试方法自动覆盖。