# 基于Pytest的UI及接口自动化测试框架 **Repository Path**: testcodes/AutomatedTesting ## Basic Information - **Project Name**: 基于Pytest的UI及接口自动化测试框架 - **Description**: 基于pytest用例管理的UI(pytest + selenium + allure)和接口(pytest + requests + allure)自动化测试平台!主打简单易用,快速上手学习! - **Primary Language**: Python - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-09-06 - **Last Updated**: 2025-09-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: pytest, 自动化测试, UI自动化测试, 接口自动化测试, Allure ## README # 自动化测试项目 ## 介绍 ### 基于pytest用例管理的UI和API的自动化测试平台 #### 1. UI自动化框架:pytest + selenium + allure #### 2. 接口自动化框架:pytest + requests + allure - 支持py、yaml、json、excel、db等常见数据驱动方式 - 将allure报告生成单个文件,方便发送 - 支持失败重试、定时执行 - 将部分常用功能进行封装,便于使用 - 主打简单易懂,快速上手 pytest 参考文档: https://pytest.cn/en/stable/ selenium 参考文档:https://www.selenium.dev/zh-cn/documentation/ allure 参考文档:https://allurereport.org/docs/ requests 参考文档:https://requests.readthedocs.io/en/latest/ ## 项目结构 - application:主项目配置目录,也存放了主路由文件 - env:基础环境配置文件 - prod.py:生产环境 - testing.py:测试环境 - routers.py:主路由文件,统一管理 - settings.py:主项目配置文件 - core:核心文件目录 - api_client.py:自定义Session类,自动记录请求和响应信息 - base_page.py:封装了常用的页面操作,可直接使用 - driver.py:浏览器驱动,根据浏览器名称动态生成 - test_dir: 测试目录 - interfaces:接口自动化测试用例目录 - datas:数据驱动文件存放目录 - test_cases:测试用例集 - menu:功能模块 - test_case_name:测试用例执行文件 - conftest.py: 接口配置文件,包含feature和其他配置代码 - website:UI自动化测试用例目录 - datas:数据驱动文件存放目录 - pages:测试页面对象服务 - menu:功能-菜单 - locators:页面定位元素 - operates:页面操作步骤 - test_cases:测试用例集 - menu:功能-菜单 目录 - test_case_name:测试用例执行文件 - conftest.py: UI配置文件,包含feature和其他配置代码 - utils:封装的一些工具方法 - logs:日志目录 - reports:测试报告存放目录 - allure_result: allure结果文件目录 - html_report: allure生成的html文件目录 - conftest.py: 全局配置文件,包含feature和其他配置代码 - environment.properties: allure环境变量 - pytest.ini: pytest配置文件 - main.py:主程序入口文件 - requirements.txt: 依赖包 ## 开发环境 开发语言:Python 3.10 ## 开发工具 Pycharm 2024.3.6 推荐插件:Chinese (Simplified) Language Pack / 中文语言包 ## 使用 ``` # 安装依赖库 pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ # 第三方源: 1. 阿里源: https://mirrors.aliyun.com/pypi/simple/ 2. 清华大学: https://pypi.tuna.tsinghua.edu.cn/simple/ 3. 中国科技大学: https://mirrors.bfsu.edu.cn/pypi/web/simple/ 4. 豆瓣: https://mirrors.cloud.tencent.com/pypi/simple/ ``` ### 执行测试用例集 ```shell # 直接运行main文件run-test命令 python main.py ``` ## 其他操作 用例规范 - 用例文件需存放于test_cases目录下,且以test开头(示例:test_login.py); - 用例写在类中时,类名称需要Test开头,类中用例名称要以test_开头; - 如果需要自定义测试文件或测试用例,可在pytest.ini中定义; 用例默认执行顺序 - 同一层级的文件名通过ASC码排序,先找到的文件,先收集里面的用例; - 不同层级的文件:从外到内,即先把外层目录下的收集完成后,在找文件夹里面的; - 文件当中的用例执行顺序:从上到下,按照代码顺序; - 需要跳过用例不执行时,在用例方法中添加装饰器@pytest.mark.skip即可; - 默认情况下,pytest会按照文件系统中的文件顺序来发现和执行测试用例。但是,你可以通过几种方式来控制测试用例的执行顺序: ``` 1. 使用装饰器控制测试用例执行顺序: @pytest.mark.order(n),其中n是一个整数,表示执行顺序(数字小的先执行); 2. 使用文件名的字母/数字顺序或自定义的规则,如:def test_01normal_login, def test_02empty_name_login。 ``` ## 代码示例: ### 1. UI自动化测试用例 - 定义页面定位元素 ```python # locators.py from selenium.webdriver.common.by import By # 通过By方法获取定位信息 class PageLocators: """页面定位元素集""" username = (By.ID, 'Username') # 用户名,通过id定位 password = (By.NAME, 'Password') # 密码,通过name定位 login_btn = (By.XPATH, '/form/div[3]/div/button') # 登录按钮,通过xpath定位 success_mark = (By.CSS_SELECTOR, '#app > div > div.app-main > div') # 登录成功标识,通过CSS定位 fail_hint = (By.XPATH, '/html/body/div/div/div[1]/form/div[3]/div/div/ul/li') # 登录失败提示内容 ``` - 定义页面操作步骤 ```python # operates.py from core.base_page import BasePage from test_dir.website.pages.login.locators import PageLocators class PageOperate(BasePage): """页面操作""" loc = PageLocators() def input_username(self, username): """ 输入登录用户名 :param username: 用户名 """ # 先清空 self.backspace_clear(self.loc.loc_username) # 再输入 self.input_value(self.loc.loc_username, username) def input_password(self, password): """ 输入登录密码 :param password: 用户密码 """ # 先清空 self.backspace_clear(self.loc.loc_password) # 再输入 self.input_value(self.loc.loc_password, password) def click_login_button(self): """点击登录按钮""" self.click_element(self.loc.loc_login_btn) def get_success_mark(self): """获取登录成功标识""" try: return self.get_element_text(self.loc.loc_success_mark) except ValueError: return '' def get_fail_mark(self): """获取登录失败标识""" # 检测页面仍停留在登录页面则表示登录失败 try: return self.get_element_text(self.loc.loc_fail_hint) except ValueError: return '' def user_logout(self): """退出登录""" # 点击个人中心选项 self.click_element(self.loc.loc_user_avatar) # 移动到退出登录选项上方并点击 self.move_and_click_element(self.loc.loc_logout) # 弹窗点击确认退出 self.click_element(self.loc.loc_logout_confirm) ``` - 编写测试用例 ```python # test_login.py import pytest import allure from test_dir.website.datas.test_data import LoginData, db_user_data # 引入驱动测试数据 from utils.error_handle import exception_monitor from utils.file_reader import * @allure.story("登录功能") class TestLogin: """登录测试""" @allure.title("用户名密码正确登录") @pytest.mark.ui @exception_monitor def test_login_normal(self, login_page): username = '15020221010' password = 'kinit2022' expect = 'kinit' # 测试步骤 with allure.step("步骤1:输入用户名"): login_page.input_username(username) with allure.step("步骤2:输入密码"): login_page.input_password(password) with allure.step("步骤3:点击登录"): login_page.click_login_button() # 断言 mark = login_page.get_success_mark() # 获取登录成功标识 assert mark == expect # 判断结果是否等于预期 # py文件做数据驱动测试 @allure.title("用户名或密码为空不能登录成功-数据驱动[ py数据 ]") @pytest.mark.parametrize("datas", LoginData.empty_data, ids=['pwd_empty', 'name_empty']) @pytest.mark.ui @exception_monitor def test01_login_empty(self, datas, login_page): with allure.step('执行登录操作'): login_page.login_opt(datas['user'], datas['pwd']) # 断言, 获取登录失败提示信息 if not datas['user']: mark = login_page.get_name_empty_text() else: mark = login_page.get_pwd_empty_text() assert mark == datas['expect'] # 判断结果是否等于预期 # yaml文件做数据驱动测试 @allure.title("用户名或密码为空不能登录成功-数据驱动[ yaml数据 ]") @pytest.mark.parametrize("datas", read_yaml(ui_data_file('test_data.yaml'), 'empty_data')) @pytest.mark.ui @exception_monitor def test02_login_empty(self, datas, login_page): with allure.step('执行登录操作'): login_page.login_opt(datas['user'], datas['pwd']) # 断言 mark = login_page.get_fail_mark() # 获取登录失败标识 assert mark == datas['expect'] # 判断结果是否等于预期 # json文件做数据驱动测试 @allure.title("用户名或密码错误不能登录成功-数据驱动[ json数据 ]") @pytest.mark.parametrize("datas", read_json(ui_data_file('test_data.json'), 'error_data')) @pytest.mark.ui @exception_monitor def test03_login_error(self, datas, login_page): with allure.step('执行登录操作'): login_page.login_opt(datas['user'], datas['pwd']) # 执行登录操作 # 断言 mark = login_page.get_fail_mark() # 获取登录失败标识 assert mark == datas['expect'] # 判断结果是否等于预期 # excel文件做数据驱动测试 @allure.title("用户名或密码错误不能登录成功-数据驱动[ excel数据 ]") @pytest.mark.parametrize("datas", read_excel(ui_data_file('test_data.xlsx'))) @pytest.mark.ui @exception_monitor def test04_login_error(self, datas, login_page): with allure.step('执行登录操作'): login_page.login_opt(datas['user'], datas['pwd']) # 执行登录操作 # 断言 mark = login_page.get_fail_mark() # 获取登录失败标识 assert mark == datas['expect'] # 判断结果是否等于预期 # 数据库数据做数据驱动测试 @allure.title("用户名或密码错误不能登录成功-数据驱动[ DB数据 ]") @pytest.mark.parametrize("datas", db_user_data()) @pytest.mark.ui @exception_monitor def test05_login_fail(self, datas, login_page): with allure.step('执行登录操作'): login_page.login_opt(datas[0], datas[1]) # 执行登录操作 # 断言 mark = login_page.get_fail_mark() # 获取登录失败标识 assert mark == datas[2] # 判断结果是否等于预期 ``` ### 2. 接口自动化测试用例 ```python # test_api_auth_users.py import pytest import allure from jsonpath import jsonpath from utils.custom_assert import * from utils.file_reader import * from test_dir.interfaces.datas.test_data import AuthUserData, db_auth_users class TestAuthUserAPI: """用户列表接口测试""" # 接口地址 api_url = "/api/vadmin/auth/users" @allure.title("获取用户列表,数据驱动[ py数据 ]") @pytest.mark.order(1) @pytest.mark.parametrize('datas', AuthUserData.api_auth_users) @pytest.mark.api def test_api_user_admin_current_info_by_py(self, api_session, datas): # 发起请求 response = api_session.get(self.api_url, params=datas['params']) # 多种断言 # 1. 响应数据是否正常 assert_response_normal(response) # 2. 响应状态码等于200 assert_status_code_equal(response) # 3. 接口错误码等于2000 assert_code_equal(response) # 4. 通过jsonpath提取需要的信息,当传入不存在的key(name)时,返回False for assertion in datas['assertions']: name = jsonpath(response.json(), assertion['expr']) assert_data_not_equal(name, False) # 5. 接口响应数据内容是否一致 assert_data_equal(name[0], assertion['value']) @allure.title("获取用户列表,数据驱动[ yaml数据 ]") @pytest.mark.order(2) @pytest.mark.parametrize('datas', read_yaml(api_data_file("test_data.yaml"), 'api_auth_users')) @pytest.mark.api def test_api_user_admin_current_info_by_yaml(self, api_session, datas): # 发起请求 response = api_session.get(self.api_url, params=datas['params']) # 多种断言 # 1. 响应数据是否正常 assert_response_normal(response) # 2. 响应状态码等于200 assert_status_code_equal(response) # 3. 接口错误码等于2000 assert_code_equal(response) # 4. 通过jsonpath提取需要的信息,当传入不存在的key(name)时,返回False for assertion in datas['assertions']: name = jsonpath(response.json(), assertion['expr']) assert_data_not_equal(name, False) # 5. 接口响应数据内容是否一致 assert_data_equal(name[0], assertion['value']) @allure.title("获取用户列表,数据驱动[ json数据 ]") @pytest.mark.order(3) @pytest.mark.parametrize('datas', read_json(api_data_file("test_data.json"), 'api_auth_users')) @pytest.mark.api def test_api_user_admin_current_info_by_json(self, api_session, datas): # 发起请求 response = api_session.get(self.api_url, params=datas['params']) # 多种断言 # 1. 响应数据是否正常 assert_response_normal(response) # 2. 响应状态码等于200 assert_status_code_equal(response) # 3. 接口错误码等于2000 assert_code_equal(response) # 4. 通过jsonpath提取需要的信息,当传入不存在的key(name)时,返回False for assertion in datas['assertions']: name = jsonpath(response.json(), assertion['expr']) assert_data_not_equal(name, False) # 5. 接口响应数据内容是否一致 assert_data_equal(name[0], assertion['value']) @allure.title("获取用户列表,数据驱动[ excel数据 ]") @pytest.mark.order(4) @pytest.mark.parametrize('datas', read_excel(api_data_file("test_data.xlsx"))) @pytest.mark.api def test_api_user_admin_current_info_by_excel(self, api_session, datas): # 发起请求 response = api_session.get(self.api_url, params=eval(datas['params'])) # 多种断言 # 1. 响应数据是否正常 assert_response_normal(response) # 2. 响应状态码等于200 assert_status_code_equal(response) # 3. 接口错误码等于2000 assert_code_equal(response) # 4. 通过jsonpath提取需要的信息,当传入不存在的key(name)时,返回False for assertion in eval(datas['assertions']): name = jsonpath(response.json(), assertion['expr']) assert_data_not_equal(name, False) # 5. 接口响应数据内容是否一致 assert_data_equal(name[0], assertion['value']) @allure.title("获取用户列表,数据驱动[ DB数据 ]") @pytest.mark.order(5) @pytest.mark.parametrize('datas', db_auth_users()) @pytest.mark.api def test_api_user_admin_current_info_by_db(self, api_session, datas): # 请求参数 params = { "name": datas[0], "telephone": datas[1], "is_active": datas[2], "is_staff": datas[3], "page": 1, "limit": 10 } # 发起请求 response = api_session.get(self.api_url, params=params) # 多种断言 # 1. 响应数据是否正常 assert_response_normal(response) # 2. HTTP状态码等于200 assert_status_code_equal(response) # 3. 接口响应状态码等于200 assert_code_equal(response) # 4. 通过jsonpath提取需要的信息,当传入不存在的key(name)时,返回False name = jsonpath(response.json(), datas[4]) assert_data_not_equal(name, False) # 5. 接口响应数据内容是否一致 assert_data_equal(name[0], datas[5]) ``` ### 3. Redis和db数据库使用介绍 ```python # redis及db使用示例 def test_db_use(rd, db): # redis可直接使用原生方法:get/set/mget/mset/delete/.... # 设置值 rd.set('access_token', 'access_token_123456......', 10800) # 获取值 rd_data = rd.get('access_token') # db使用原生增删改查命令调用,传入原生SQL语句即可 # 插入数据 db.insert("""INSERT INTO db_users (name, age) VALUES ('Jack', 22);""") # 修改数据 db.update("""UPDATE db_users SET age = 21 WHERE id = 1;""") # 查询数据 db_data = db.select("""SELECT name,age FROM db_users WHERE id = 1;""") # 删除数据 db.delete("""DELETE FROM db_users WHERE id = 1;""") return rd_data, db_data ``` ### 4. 定时执行测试 - 参照utils.task_scheduler.py定时任务示例; - 支持4种执行方式:立即执行、定时执行、Cron时间执行、间隔时间执行; - 定时服务启动后,访问 http://127.0.0.1:8001/jobs 接口地址,即可查询执行中的定时任务列表; ### 5. 邮件发送 - 邮件发送方法已进行封装,详见 utils.send_email.py - 在 application.settings.py 中配置邮件参数; - 调用 Email.send 方法传入邮件内容即可实现邮件发送,可根据需要自行配置。 ### 6. 其他功能 - 后续根据使用情况逐步完善... ## 测试报告 ![img.png](static/report.png) ![img_1.png](static/report_1.png)