
unittest 的 setUp/tearDown 是函数级作用域,无法灵活共享。而pytest的fixture本质是一个依赖注入容器。
源码级对比:
unittest 在每个测试方法执行前后强制调用,资源创建与销毁紧耦合。pytest 的 fixture 通过装饰器 + 生成器(yield) 实现懒加载和Teardown反置。import pytest
import pymysql
@pytest.fixture(scope="session") # session级别:整个测试会话只创建一次
def db_connection():
"""数据库连接fixture,自动管理生命周期"""
conn = pymysql.connect(
host="test_db_host",
user="tester",
password="tester_pwd",
database="test_db"
)
yield conn # 此处返回连接,测试执行到这里暂停
conn.close() # 测试全部结束后执行清理
def test_insert_order(db_connection):
cursor = db_connection.cursor()
cursor.execute("INSERT INTO orders(order_id) VALUES('T001')")
db_connection.commit()
# 无需手动close,fixture的yield后代码自动执行深度洞见:yield 的实现依赖Python生成器的协程切换机制。pytest 框架在运行时会维护一个 fixture 请求栈,确保依赖顺序和资源回收的正确性。
assertpytest 的断言重写机制(assertion rewriting)是其杀手锏。在字节码层面,pytest 会劫持Python的 assert 语句,重写成包含详细上下文信息的版本。
# 你的代码
assert response.json()["data"]["status"] == "SUCCESS"
# pytest重写后的等效逻辑(伪代码)
__assertion_context = {
'expression': 'response.json()["data"]["status"] == "SUCCESS"',
'left': response.json()["data"]["status"], # 实际值
'right': 'SUCCESS'
}
if __assertion_context['left'] != __assertion_context['right']:
raise AssertionError(f"实际值: {__assertion_context['left']}, 期望值: SUCCESS")这种实现使断言失败时能直接打印变量实际值,无需手动拼接错误信息。
真正的全栈自动化必须覆盖业务链路,而非孤立的单点测试。我们采用三层数据驱动模型:
企业微服务通常有复杂的Token鉴权和超时重试机制。我们封装一个 APIClient 基类:
import requests
from tenacity import retry, stop_after_attempt, wait_exponential
class APIClient:
def __init__(self, base_url, auth_token=None):
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({"Authorization": f"Bearer {auth_token}"})
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def post_with_retry(self, endpoint, json_data):
"""带指数退避的重试POST,解决网络波动"""
response = self.session.post(
f"{self.base_url}/{endpoint}",
json=json_data,
timeout=5
)
response.raise_for_status() # 抛出HTTPError
return responsetenacity 重试原理:@retry 装饰器在函数抛出异常时,会捕获并进入等待循环,底层依赖 time.sleep 和 sys.exc_info 捕获异常栈,在超时或重试次数耗尽后重新抛出原始异常。
传统Page Object模式会导致大量样板代码。我们引入元素定位与动作分离的 Element 基类:
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_and_click(self, locator):
"""智能等待+点击,内嵌重试机制"""
for _ in range(3):
try:
element = self.wait.until(EC.element_to_be_clickable(locator))
element.click()
return
except Exception:
# 处理元素被遮挡/状态变化等场景
continue
raise Exception(f"元素 {locator} 无法点击")
class LoginPage(BasePage):
# 定位器策略:DATA驱动 + 显性等待
USERNAME_INPUT = ("id", "username")
PASSWORD_INPUT = ("css", "[data-test='password']")
SUBMIT_BTN = ("xpath", "//button[text()='登录']")
def login(self, user, pwd):
self.find_and_click(self.USERNAME_INPUT).send_keys(user)
self.find_and_click(self.PASSWORD_INPUT).send_keys(pwd)
self.find_and_click(self.SUBMIT_BTN)
# 返回下一个页面对象,实现链式调用
return DashboardPage(self.driver)核心原理:
WebDriverWait底层使用polling2机制,每500ms轮询一次条件,直到超时。expected_conditions本质是返回一个可调用对象,传入driver执行断言。
测试最怕“脏数据”,我们通过 “测试数据工厂 + 事务回滚” 策略保障隔离性:
@pytest.fixture
def clean_db(db_connection):
"""事务级Fixture:每个测试用例独立事务,结束后回滚"""
db_connection.begin() # 开启事务
yield db_connection
db_connection.rollback() # 测试结束后强制回滚
def test_create_order(clean_db):
cursor = clean_db.cursor()
cursor.execute("INSERT INTO orders(order_id) VALUES('T001')")
clean_db.execute("SELECT * FROM orders WHERE order_id='T001'")
assert cursor.fetchone() is not None
# 退出测试后,事务回滚,数据库中无该条记录为什么用事务回滚而非DELETE? DELETE 可能触发数据库约束或触发器,且无法应对分布式事务的延迟。事务回滚直接丢弃所有变更,效率更高,完全隔离。
当用例数超过2000个,单机执行成为瓶颈。我们使用 pytest-xdist + Redis 实现分布式调度。
pytest-xdist 原理是:
execnet 将用例分发到多个Worker进程。-n auto 自动检测CPU核数,--dist loadscope 按模块分发避免重复加载fixture。pytest -n 8 --dist loadscope --maxfail=5底层负载均衡策略:
--dist loadscope 会将同一个文件下的用例分发给同一个Worker,最大程度复用 session 级别的 fixture 缓存。分布式执行最大的挑战是测试用例状态聚合。我们将每个用例的执行结果(passed/failed/skipped)通过Redis的 list 结构推入汇总队列:
import redis
import json
r = redis.Redis(host='test-redis', decode_responses=True)
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_makereport(item, call):
"""pytest钩子:在每个测试用例完成后触发"""
outcome = "passed" if call.excinfo is None else "failed"
report = {
"test_id": item.nodeid,
"outcome": outcome,
"duration": call.duration,
"worker": os.getpid()
}
r.rpush("test_reports", json.dumps(report))最终在Master节点拉取所有报告,生成带失败截图和接口耗时的Allure聚合报告。
测试跑得快还不够,必须有卡点策略防止烂代码上线。
pipeline {
agent any
stages {
stage('Build & Unit Test') {
steps {
sh 'pytest --cov=src --cov-report=xml'
sh 'coverage xml -o coverage.xml' // 生成覆盖率报告
}
}
stage('API & UI Full Regression') {
steps {
sh 'pytest -n 4 --alluredir=allure-results'
}
}
}
post {
always {
allure includeProperties: false, jdk: '', results: [[path: 'allure-results']]
junit 'reports/*.xml'
}
unstable {
// 当测试通过率低于95%时,标记为不稳定
sh 'pytest --passed-percentage=95'
}
failure {
// 发送钉钉告警
dingtalk(webhook: 'your-webhook', message: '构建失败!')
}
}
}coverage.py + pytest-cov)。pytest-benchmark 跟踪)。现象:单独运行用例全部通过,全量跑时出现 AssertionError。
根因:pytest 默认收集顺序导致某些 fixture 在多个用例间共享了可变对象(如 list)。
解决:在 conftest.py 中设置 @pytest.fixture(scope="function") 强制每次调用重新创建,或使用 deepcopy 返回副本。
根因:每个Worker使用独立 driver 对象,但 WebDriver 本身不是进程安全的。
解决:在 worker_id 的 fixture 中动态指定 webdriver.Remote 的 command_executor,为每个Worker分配独立的 node 标签:
@pytest.fixture(scope="module")
def driver(worker_id):
# worker_id 由 xdist 提供,形如 'gw0', 'gw1'
driver = webdriver.Remote(
command_executor=f'http://selenium-hub:4444/wd/hub',
desired_capabilities={
'browserName': 'chrome',
'selenoid:options': {'name': worker_id}
}
)
yield driver
driver.quit()Python全栈自动化测试的精髓,不在于用多少框架,而在于是否构建了一个 “可维护、可观测、可演进” 的体系:
Type Hints)和 ruff 格式检查,保持代码健康度。最后,分享一个真实数据:这套平台落地后,线上故障率下降72%,发布频率从“每周一次”提升至“每天三次”。全栈自动化测试的价值,是让测试从“质量验证”升级为“质量内建”。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。