Python+Selenium自动化测试框架:Allure报告生成实战指南
1. 项目概述:为什么我们需要自动化生成测试报告?
做自动化测试的朋友,尤其是用Python和Selenium的,肯定都经历过这个阶段:脚本跑得飞起,用例执行得也挺顺利,但一到写报告的时候就头疼。要么是手动整理Excel,复制粘贴一堆截图和日志,格式还容易乱;要么是脚本输出的日志文件一大堆,需要二次加工才能给领导或团队看。这个过程不仅枯燥、容易出错,而且严重拖慢了整个测试反馈的闭环速度。
我干了十多年测试,从手动点点点到全流程自动化,最深的一个体会就是: 自动化测试的价值,一半在于执行,另一半在于清晰、高效的结果呈现。 一个不能自动生成漂亮、专业报告的自动化框架,就像一个只会埋头干活但不会汇报的员工,价值大打折扣。Python + Selenium的组合,让我们能轻松操控浏览器,模拟用户操作。但光有“自动化执行”还不够,我们必须把“自动化报告”这个短板补上。
这个项目的核心,就是解决这个痛点。它不是一个简单的日志打印,而是一个完整的、可定制的、能直接用于交付的测试报告生成方案。想象一下,每天定时任务跑完几百个用例,第二天早上你喝咖啡的时候,一份包含通过率、失败详情、错误截图、执行时长、趋势图表的HTML报告已经静静地躺在你的邮箱或者共享目录里。这不仅能极大提升个人和团队的工作效率,更能让测试工作的价值可视化,为项目决策提供即时、可靠的数据支持。
接下来,我会带你从零开始,手把手搭建一个基于Python + Selenium,并能自动生成精美测试报告的实战框架。我们会用到一些非常主流且强大的库,比如 pytest 作为测试运行器, Allure 或者 HTMLTestRunner 来生成报告。我会重点讲解其中的设计思路、关键配置、以及我踩过的那些坑,保证你跟着做就能复现。
2. 框架整体设计与核心组件选型
在动手写代码之前,我们先得把蓝图规划好。一个健壮的自动化测试框架,尤其是要集成报告功能的,不能是脚本的简单堆砌。我们需要考虑可维护性、可扩展性和易用性。
2.1 核心架构分层
我习惯将框架分为四层,这样结构清晰,职责分明:
-
基础层(Base Layer) :这是框架的基石。主要包含对Selenium WebDriver的二次封装。比如,我们会在这里统一浏览器的初始化(Chrome, Firefox等)、封装常用的等待机制(显式等待)、通用的元素查找和操作函数(点击、输入、获取文本等)。这样做的好处是,上层的测试用例脚本会非常干净,只关心业务逻辑,而不必处理
WebDriverWait、NoSuchElementException这些底层细节。当浏览器驱动或Selenium API有变动时,我们只需要修改这一层的代码。 -
页面对象层(Page Object Layer, PO) :这是实现测试用例与页面代码分离的关键设计模式。每个页面对应一个类,类里面的属性代表页面元素(定位符),方法代表在该页面上的操作(如登录、搜索、添加购物车)。测试用例层通过调用这些页面对象的方法来组合业务流程。PO模式极大地提高了代码的可读性和可维护性。当页面UI发生变化时,通常只需要更新对应页面对象类中的元素定位符,而不需要修改大量的测试用例。
-
测试用例层(Test Case Layer) :这一层就是我们的
pytest测试函数或测试类。它们应该非常简洁,读起来像自然语言一样,描述的是一个完整的测试场景。例如:test_login_with_valid_credentials。在这一层,我们调用页面对象的方法,并使用assert语句进行断言。 报告所需的大部分信息,如测试步骤、断言结果,都在这层产生。 -
报告与执行层(Report & Execution Layer) :这是本项目的重点。我们利用
pytest的钩子函数(hooks)和插件系统,在测试执行的生命周期中(开始、结束、用例通过/失败)插入我们的逻辑。核心任务是:收集测试结果(通过、失败、跳过、错误)、捕获失败时的屏幕截图、收集测试日志,并将这些数据交给报告生成器(如Allure)去渲染成最终的HTML报告。
2.2 关键工具选型与理由
为什么是 pytest + Allure ?这是我经过多年对比后的选择。
-
测试运行器:Pytest
- 优势 :语法极其简洁(直接用
assert),夹具(fixture)功能强大(完美管理测试前置和后置条件,如初始化/关闭浏览器),插件生态丰富,参数化测试非常方便。 - 对比 :相比Python自带的
unittest框架,pytest更灵活、更“Pythonic”,社区活跃度也更高。对于集成报告生成来说,pytest的插件机制让我们更容易捕获到详细的测试执行信息。
- 优势 :语法极其简洁(直接用
-
报告生成器:Allure
- 优势 :这是目前业界公认最强大、最漂亮的测试报告工具之一。它生成的HTML报告是交互式的,支持按特性、故事、严重等级等多维度分类查看。可以展示详细的测试步骤、附件(截图、日志、请求数据)、历史趋势图,并且支持与CI/CD工具(如Jenkins)无缝集成。
- 工作原理 :
pytest通过allure-pytest插件执行测试。测试运行时,插件会在项目目录下生成一个临时的allure-results文件夹,里面是以JSON格式存储的原始测试结果数据。测试结束后,我们使用allure命令行工具,读取这个文件夹,生成最终的HTML报告。 - 备选方案 :如果你觉得Allure稍微重了一点,或者环境配置有困难,
HTMLTestRunner是一个经典的、轻量级的替代品。它是一个单独的Python文件,可以直接集成到unittest框架中,生成基础的HTML报告。但它在美观度、交互性和功能扩展性上远不如Allure。
注意 :Allure本身是一个Java工具,需要本地安装Java环境(JDK 8+)。但别担心,这对Python项目使用来说不是障碍,我们只是用它来生成报告。
确定了核心架构和工具,我们就可以开始搭建环境了。
3. 环境搭建与核心配置详解
工欲善其事,必先利其器。这一步看似基础,但很多坑都埋在这里。我会把每一步的意图和常见问题都讲清楚。
3.1 创建虚拟环境与安装依赖
永远不要在系统的全局Python环境里直接安装项目依赖。使用虚拟环境( venv 或 conda )是专业开发的第一步,它能避免包版本冲突。
# 1. 创建项目目录并进入
mkdir selenium-automation-report
cd selenium-automation-report
# 2. 创建虚拟环境(以venv为例)
python -m venv venv
# 3. 激活虚拟环境
# 在Windows上:
venv\Scripts\activate
# 在Mac/Linux上:
source venv/bin/activate
# 激活后,命令行提示符前会出现 (venv) 标识
接下来,创建 requirements.txt 文件,列出所有依赖。这是项目可复现性的关键。
# requirements.txt
selenium>=4.0.0
pytest>=7.0.0
allure-pytest>=2.9.0
webdriver-manager>=3.8.0
pytest-html>=3.2.0 # 可选,一个简单的HTML报告插件
使用 pip 安装:
pip install -r requirements.txt
依赖包说明:
selenium: 核心自动化库。pytest: 测试框架。allure-pytest: 连接pytest和Allure的桥梁插件。webdriver-manager: 强烈推荐! 这个神器可以自动下载和管理ChromeDriver、GeckoDriver等浏览器驱动,无需手动下载和配置PATH环境变量,彻底解决“驱动版本不匹配”的噩梦。pytest-html: 一个轻量级插件,能快速生成一个简单的HTML报告,可以作为Allure的补充或快速预览。
3.2 安装Allure命令行工具
Allure报告生成需要命令行工具。请根据你的操作系统,从 Allure官网 下载并安装。或者使用包管理器(如Mac的 brew install allure )。安装后,在命令行输入 allure --version 能显示版本号即表示成功。
3.3 设计项目目录结构
一个清晰的项目结构是良好维护的开始。我推荐如下结构:
selenium-automation-report/
├── config/ # 配置文件目录
│ └── config.yaml # 存放URL、账号、超时时间等配置
├── logs/ # 日志文件目录(运行时自动生成)
├── reports/ # 测试报告目录(运行时自动生成)
│ ├── allure-results/ # Allure原始结果数据
│ └── html/ # 最终生成的HTML报告
├── pages/ # 页面对象层
│ ├── __init__.py
│ ├── base_page.py # 基类,封装通用方法
│ ├── login_page.py # 登录页面
│ └── home_page.py # 主页
├── test_cases/ # 测试用例层
│ ├── __init__.py
│ ├── conftest.py # pytest共享夹具配置
│ └── test_login.py # 登录相关测试用例
├── utils/ # 工具函数
│ ├── __init__.py
│ ├── logger.py # 日志记录器
│ └── screenshot.py # 截图工具
├── requirements.txt # 项目依赖
└── run_tests.py # 主运行脚本
这个结构将不同职责的代码模块化,未来无论项目变得多复杂,都能保持井然有序。
4. 核心代码实现:从封装到报告生成
现在,我们开始填充这个骨架,编写核心代码。我会挑最关键的几个文件详细讲解。
4.1 基础层:封装BasePage
pages/base_page.py 是所有页面对象的父类,它封装了所有与Selenium交互的通用操作。
# pages/base_page.py
import logging
from datetime import datetime
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
class BasePage:
def __init__(self, driver):
self.driver = driver
self.logger = logging.getLogger(__name__)
self.timeout = 10 # 默认显式等待超时时间
def find_element(self, locator):
"""查找单个元素,加入显式等待"""
try:
self.logger.info(f"正在查找元素: {locator}")
element = WebDriverWait(self.driver, self.timeout).until(
EC.presence_of_element_located(locator)
)
return element
except TimeoutException:
self.logger.error(f"查找元素超时: {locator}")
# 这里可以调用截图函数,我们稍后实现
raise
def click(self, locator):
"""点击元素"""
element = self.find_element(locator)
self.logger.info(f"点击元素: {locator}")
element.click()
def input_text(self, locator, text):
"""向元素输入文本"""
element = self.find_element(locator)
self.logger.info(f"向元素 {locator} 输入文本: {text}")
element.clear()
element.send_keys(text)
def get_text(self, locator):
"""获取元素文本"""
element = self.find_element(locator)
text = element.text
self.logger.info(f"获取元素 {locator} 的文本: {text}")
return text
def is_element_visible(self, locator, timeout=None):
"""判断元素是否可见"""
wait_time = timeout or self.timeout
try:
WebDriverWait(self.driver, wait_time).until(
EC.visibility_of_element_located(locator)
)
return True
except TimeoutException:
return False
关键点解析:
- 显式等待 :
WebDriverWait配合expected_conditions是处理动态页面加载的最佳实践。它比固定的sleep和隐式等待更智能、更高效。 - 日志记录 :每个操作都通过
logging模块记录日志,这对于后期排查问题至关重要。日志会作为附件添加到Allure报告中。 - 异常处理 :对
TimeoutException进行了捕获和日志记录,并重新抛出,确保测试用例能正确捕获失败。
4.2 页面对象层:实现LoginPage
以登录页面为例,展示如何继承 BasePage 。
# pages/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(self.LOGIN_BUTTON)
def get_error_message(self):
"""获取登录错误信息"""
if self.is_element_visible(self.ERROR_MSG, timeout=3):
return self.get_text(self.ERROR_MSG)
return None
设计精髓 :所有元素定位符都定义为类属性。如果页面UI改了,比如登录按钮的ID变了,你只需要来这个文件修改这一行代码,所有用到这个按钮的测试用例都自动生效。
4.3 测试用例层:编写Pytest测试
test_cases/conftest.py 是pytest的本地插件文件,用于定义在整个测试目录共享的 fixture ,比如驱动初始化。
# test_cases/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from utils.logger import setup_logger
@pytest.fixture(scope="function") # 每个测试函数执行一次
def driver():
"""初始化WebDriver夹具"""
# 使用webdriver-manager自动管理驱动
service = Service(ChromeDriverManager().install())
options = webdriver.ChromeOptions()
options.add_argument('--headless') # 无头模式,不打开浏览器窗口,适合CI环境
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome(service=service, options=options)
driver.maximize_window()
driver.implicitly_wait(5) # 设置全局隐式等待,作为兜底策略
yield driver # 将driver对象提供给测试用例
driver.quit() # 测试结束后退出浏览器
@pytest.fixture(scope="session", autouse=True)
def setup_logging():
"""设置全局日志,自动执行"""
setup_logger()
现在,可以编写真正的测试用例了。
# test_cases/test_login.py
import allure
import pytest
from pages.login_page import LoginPage
from pages.home_page import HomePage
@allure.epic("Web自动化测试")
@allure.feature("登录模块")
class TestLogin:
@allure.story("成功登录")
@allure.title("使用正确的用户名和密码登录系统")
@allure.severity(allure.severity_level.CRITICAL)
def test_login_success(self, driver):
"""测试正常登录流程"""
login_page = LoginPage(driver)
home_page = HomePage(driver)
with allure.step("1. 打开登录页面"):
driver.get("https://your-test-site.com/login")
allure.attach(driver.get_screenshot_as_png(), name="登录页面截图", attachment_type=allure.attachment_type.PNG)
with allure.step("2. 输入正确的用户名和密码"):
login_page.login("valid_user", "valid_pass")
with allure.step("3. 验证登录成功,跳转到首页"):
assert home_page.is_user_logged_in("valid_user"), "登录后未显示正确的用户名"
allure.attach(driver.get_screenshot_as_png(), name="登录成功首页截图", attachment_type=allure.attachment_type.PNG)
@allure.story("登录失败")
@allure.title("使用错误的密码登录系统")
def test_login_failure_wrong_password(self, driver):
"""测试密码错误登录失败"""
login_page = LoginPage(driver)
driver.get("https://your-test-site.com/login")
login_page.login("valid_user", "wrong_pass")
error_msg = login_page.get_error_message()
with allure.step("验证错误提示信息"):
assert error_msg is not None, "未出现错误提示信息"
assert "密码错误" in error_msg, f"错误信息不符,实际为: {error_msg}"
allure.attach(driver.get_screenshot_as_png(), name="错误提示截图", attachment_type=allure.attachment_type.PNG)
Allure装饰器详解:
@allure.epic/feature/story:用于在报告中分层级组织测试用例,类似于敏捷开发中的特性-故事。@allure.title:为测试用例设置一个更易读的标题,覆盖默认的函数名。@allure.severity:标记测试用例的严重等级(BLOCKER, CRITICAL, NORMAL, MINOR, TRIVIAL),便于在报告中按优先级筛选。@allure.step: 这是生成步骤化报告的关键! 它将测试用例分解为多个可读的步骤,在Allure报告中会清晰地展示出来。allure.attach:用于在报告中添加附件,这里我们添加了失败时的截图。截图是定位UI问题最直观的证据。
4.4 报告生成层:集成与执行
最后,我们需要一个统一的入口来运行测试并生成报告。创建 run_tests.py 。
# run_tests.py
import os
import subprocess
import sys
import shutil
from datetime import datetime
def run_tests():
"""执行测试并生成Allure报告"""
# 定义路径
allure_results_dir = "./reports/allure-results"
allure_report_dir = "./reports/html"
# 清理旧的测试结果数据(可选,首次运行可跳过)
if os.path.exists(allure_results_dir):
shutil.rmtree(allure_results_dir)
os.makedirs(allure_results_dir, exist_ok=True)
# 使用pytest执行测试,并指定Allure结果存储目录
# -v: 详细输出
# -s: 允许终端输出(如print语句)
# --alluredir: 指定Allure结果目录
pytest_cmd = [
sys.executable, "-m", "pytest",
"test_cases/",
"-v",
"-s",
f"--alluredir={allure_results_dir}",
# "--html=./reports/pytest_report.html", # 可同时生成pytest-html报告
# "--self-contained-html",
]
print("开始执行自动化测试...")
exit_code = subprocess.call(pytest_cmd)
print(f"测试执行完成,退出码: {exit_code}")
# 生成Allure HTML报告
if os.path.exists(allure_results_dir) and os.listdir(allure_results_dir):
print("正在生成Allure报告...")
generate_cmd = ["allure", "generate", allure_results_dir, "-o", allure_report_dir, "--clean"]
subprocess.call(generate_cmd)
print(f"报告已生成,请打开文件查看: {os.path.abspath(allure_report_dir)}/index.html")
# 尝试自动打开报告(仅限本地开发环境)
try:
if sys.platform == "win32":
os.startfile(f"{allure_report_dir}/index.html")
elif sys.platform == "darwin":
subprocess.call(["open", f"{allure_report_dir}/index.html"])
except:
pass
else:
print("未找到测试结果数据,报告生成失败。")
if __name__ == "__main__":
run_tests()
运行这个脚本,一切顺利的话,测试会自动执行,并在 ./reports/html/ 目录下生成一个 index.html 文件。用浏览器打开它,你就能看到交互式的Allure报告了。
5. 报告优化与高级技巧
基础框架跑通后,我们可以进一步优化报告的质量和实用性。
5.1 自动截图与失败重试
失败时自动截图是刚需。我们可以通过 pytest 的钩子函数 pytest_runtest_makereport 来实现,这样无需在每个测试步骤手动添加 allure.attach 。
在 conftest.py 中添加:
# test_cases/conftest.py (追加内容)
import pytest
from selenium import webdriver
@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:
# 尝试从夹具中获取driver对象
driver_fixture = item.funcargs.get('driver', None)
if driver_fixture is not None and isinstance(driver_fixture, webdriver.Remote):
allure.attach(
driver_fixture.get_screenshot_as_png(),
name="失败截图",
attachment_type=allure.attachment_type.PNG
)
为了增加测试的稳定性,我们还可以引入失败重试机制。安装 pytest-rerunfailures 插件,并在 pytest 命令或 pytest.ini 配置文件中添加 --reruns 2 (重试2次),这样偶发性的网络或环境问题就不会导致用例误报失败了。
5.2 丰富报告内容:环境信息与分类标签
Allure报告可以展示测试环境信息,这在与团队分享时非常有用。创建一个 environment.properties 文件在 allure-results 目录下(可以通过脚本自动生成):
# 在run_tests.py中生成此文件
env_info = f"""Browser=Chrome
Browser.Version=Latest
Python.Version={sys.version}
OS={sys.platform}
Test.Environment=Staging
Project=Selenium Automation Demo
"""
with open(os.path.join(allure_results_dir, 'environment.properties'), 'w') as f:
f.write(env_info)
此外,善用 @allure.tag 可以为测试用例打上自定义标签,比如 @allure.tag("smoke") (冒烟测试)、 @allure.tag("regression") (回归测试),方便在报告中过滤和查看。
5.3 集成到CI/CD流程
自动化测试的最终归宿是持续集成。以Jenkins为例,你需要在Jenkins上安装Allure插件。然后在Jenkins项目的构建后操作中,添加“Allure Report”步骤,指定 allure-results 目录的路径。这样,每次构建完成后,Jenkins job的页面就会直接展示Allure报告。
在 run_tests.py 中,我们可以通过环境变量来判断是否在CI环境中,从而调整行为(比如使用无头模式、不自动打开报告等)。
6. 常见问题排查与实战心得
这条路我踩过不少坑,这里总结几个最常见的问题和解决方案。
6.1 元素定位失败:永恒的难题
这是Selenium自动化中最常见的问题,没有之一。
- 问题 :
NoSuchElementException,TimeoutException。 - 排查思路 :
- 确认定位器 :首先用浏览器的开发者工具(F12)的Console里执行
$x('your_xpath')或$$('your_css')验证你的XPath或CSS选择器是否正确。 - 检查等待 :元素还没加载出来你就去操作了。 务必使用显式等待(
WebDriverWait) 代替sleep和过度依赖隐式等待。确保等待的条件是合适的(如元素可见visibility_of_element_located、可点击element_to_be_clickable)。 - 检查Frame/Iframe :如果元素在
<iframe>里面,你必须先driver.switch_to.frame(frame_reference)切换到对应的frame里,才能定位到里面的元素。操作完记得driver.switch_to.default_content()切回来。 - 检查新窗口/标签页 :点击后打开了新窗口?需要用
driver.switch_to.window(driver.window_handles[-1])切换到新窗口。 - 动态ID/Class :有些前端框架(如React, Vue)会生成随机的ID。避免使用包含动态哈希值的属性定位。寻找更稳定的父元素,或使用包含部分文本的XPath(如
//button[contains(text(), '提交')]),但需注意其稳定性。
- 确认定位器 :首先用浏览器的开发者工具(F12)的Console里执行
实操心得 :我习惯为重要的页面元素(如登录按钮、核心表单)准备2-3套定位方案(如ID、CSS、XPath),并在
BasePage的查找函数里实现一个简单的“重试机制”,按顺序尝试,直到找到一个可用的。这能显著提高脚本在UI微调时的健壮性。
6.2 驱动版本与浏览器不匹配
- 问题 :
SessionNotCreatedException: This version of ChromeDriver only supports Chrome version ... - 解决方案 :这就是为什么我强烈推荐使用
webdriver-manager。它会在运行时自动检查本地Chrome版本,并下载匹配的ChromeDriver,完美解决版本问题。 永远不要再手动下载和配置驱动了。
6.3 Allure报告生成失败或为空
- 问题 :执行成功,但
allure-results文件夹为空,或生成报告失败。 - 排查 :
- 确认
--alluredir参数路径正确,且pytest命令确实执行了测试用例(检查控制台输出)。 - 确认
allure命令行工具已正确安装并加入系统PATH。在终端直接运行allure --version测试。 - 检查
conftest.py和测试用例中是否正确导入了allure并使用了装饰器(如@allure.step)。没有这些,Allure收集不到足够的信息。 - 清理旧的
allure-results目录再试一次。
- 确认
6.4 测试执行速度慢
- 优化点 :
- 使用无头模式 :在CI环境或不需要观察UI时,为浏览器选项添加
--headless。 - 优化等待 :减少全局隐式等待时间,多用精准的显式等待。避免不必要的
sleep。 - 复用浏览器会话 :对于登录等耗时操作,可以考虑使用
scope="session"的fixture来初始化一次浏览器,所有测试用例共用。但要注意用例之间的状态隔离(清理cookies/localStorage)。 - 并行执行 :
pytest支持通过pytest-xdist插件并行运行测试。可以显著缩短大规模测试集的执行时间。
- 使用无头模式 :在CI环境或不需要观察UI时,为浏览器选项添加
6.5 测试数据管理
不要把测试数据(用户名、密码、商品ID)硬编码在测试脚本里。应该使用配置文件(如 config.yaml )或外部数据源(Excel, JSON, 数据库)。
# config/config.yaml
test_env: &default
base_url: "https://staging.example.com"
credentials:
valid_user:
username: "test_user"
password: "secure_pass123"
invalid_user:
username: "wrong_user"
password: "wrong_pass"
prod_env:
<<: *default
base_url: "https://prod.example.com"
# 覆盖生产环境的凭证...
在 conftest.py 中读取配置,并通过 fixture 提供给测试用例。这样,切换测试环境(测试/预发/生产)只需要改一个配置项。
搭建这样一个完整的Python + Selenium自动化测试报告框架,初期需要一些投入,但一旦建成,它将成为你日常测试工作中不可或缺的利器。它带来的不仅是效率的提升,更是测试过程规范化和结果专业化的体现。希望这篇超详细的指南能帮你少走弯路,快速搭建起属于自己的自动化测试体系。记住,关键不是一次写完所有代码,而是先跑通一个最简单的流程(打开页面,点击一下,生成报告),然后在此基础上,像搭积木一样,逐步完善页面对象、添加更多用例、优化报告和CI集成。动手开始吧!
更多推荐
所有评论(0)