当前位置: 首页 > news >正文

软件测试经理工作日常随记【7】-接口+UI自动化(多端集成测试)

软件测试经理工作日常随记【7】-UI自动化(多端集成测试)

自动化测试前篇在此

前言

今天开这篇的契机是,最近刚好是运维开发频繁更新证书的,每次更新都在0点,每次一更新都要走一次冒烟流程。为了不让我的美容觉被阉割!(bushi)为了方便同事儿,不用每次更新都求爷告奶地通知大家辛苦半夜走一遍测试。我紧赶慢赶,于是有了此。因为此次冒烟涉及三个端,其中两个端采用接口自动化,另外一个端采用UI自动化,集成运行。

正文

工具类

用于UI自动化的工具类
# utils.py 用于UI自动化的工具类,包含在pc端有界面的执行和linux服务器无界面的执行(本段代码以linux服务器无界面运行为例)
"""
设置驱动
驱动停止
获取弹窗信息
获取data数据
"""
import json
import timeimport allure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from log.log import Loggerclass UtilsDriver:@classmethoddef get_driver(cls):""":return:浏览器驱动"""if cls._driver is None:# 创建一个ChromeOptions对象chrome_options = Options()  # (linux服务运行1)# 添加--headless参数来启用无头模式chrome_options.add_argument("--headless")  # (linux服务运行2)# 指定ChromeDriver的路径(如果不在PATH环境变量中)# driver_path = '/path/to/chromedriver'# driver = webdriver.Chrome(executable_path=driver_path, options=chrome_options)# 如果ChromeDriver已经在PATH中,或者你可以直接调用它,则可以省略executable_pathcls._driver = webdriver.Chrome(options=chrome_options)  # (linux服务运行3)# cls._driver = webdriver.Chrome()  # 谷歌(如在pc电脑有UI界面则不执行以上服务器执行的步骤,执行该行代码)cls._driver.maximize_window()cls._driver.implicitly_wait(5)cls._driver.get("http://********") time.sleep(1)return cls._driver@classmethod"""关闭浏览器驱动"""def quit_driver(cls):if cls._driver is not None:cls.get_driver().quit()cls._driver = None@classmethod"""获取元素信息:用于断言"""def get_mes(cls, xpath):return UtilsDriver.get_driver().find_element(By.XPATH, xpath).text@classmethoddef get_mes_wait(cls, xpath):"""显性等待获取元素信息"""wait = WebDriverWait(UtilsDriver.get_driver(), 10, 1)element = wait.until(lambda x: x.find_element(By.XPATH, xpath))return element@classmethoddef get_mes_s(cls, xpath):""":param xpath: 元素的路径:return: 返回的是以元素列表,不可以直接取text,只能用for循环历遍各个元素并读取文本值"""eles = UtilsDriver.get_driver().find_elements(By.XPATH, xpath)alist = []for ele in eles:ele_mes = ele.textalist.append(ele_mes)print(alist)return alist@classmethod"""显性等待获取元素定位"""def get_element_utils_wait(cls, location):  # page页对象层的基类,显式等待wait = WebDriverWait(UtilsDriver.get_driver(), 10, 1)element_wait = wait.until(lambda x: x.find_element(By.XPATH, location))return element_wait@classmethoddef get_elements(cls, xpath):""":param xpath: 表示元素定位的路径:return: 返回找到的元素"""return UtilsDriver.get_driver().find_elements(By.XPATH, xpath)@classmethoddef get_attribute(cls, xpath, attribute):"""以元素的属性值来断言,断言前必须延迟2s:param xpath: 找到元素的路径,只取到前面的标签,然后根据标签中的元素名来判断属性值对不对:param attribute: 标签中的元素名:return: 属性值"""return UtilsDriver.get_driver().find_element(By.XPATH, xpath).get_attribute(attribute)@classmethoddef get_text(cls, xpath, expected_msg, xpath2, expected_msg2):"""有两个断言元素获取元素的文本来断言,断言前必须延迟2s:param xpath: 定位元素的路径1:param expected_msg: 断言参数1:param xpath2: 定位元素的路径2:param expected_msg2: 断言参数2"""actual_mes = UtilsDriver.get_driver().find_element(By.XPATH, xpath).textactual_mes2 = UtilsDriver.get_driver().find_element(By.XPATH, xpath2).textprint("生成截图")allure.attach(UtilsDriver.get_driver().get_screenshot_as_png(), "截图",allure.attachment_type.PNG)print("第一个断言的实际mes:" + actual_mes + "第一个断言的预期结果" + expected_msg)Logger.logger_in().info("第一个断言的实际mes:" + actual_mes + ";第一个断言的预期结果" + expected_msg)print("第二个断言的实际mes:" + actual_mes2 + "第二个断言的预期结果:" + expected_msg2)Logger.logger_in().info("第二个断言的实际mes:" + actual_mes2 + ";第二个断言的预期结果:" + expected_msg2)assert expected_msg in actual_mesprint("1断言成功!")Logger.logger_in().info('1断言成功!')assert expected_msg2 in actual_mes2print("2断言成功!")Logger.logger_in().info('2断言成功!')@classmethoddef get_text_1(cls, xpath, expected_msg):"""有一个断言元素获取元素的文本来断言,断言前必须延迟2s:param xpath:找到元素的路径,只取到前面的标签,然后根据标签中的元素名来判断属性值对不对:param expected_msg:期待定位的元素获取的值"""actual_mes = UtilsDriver.get_driver().find_element(By.XPATH, xpath).text# actual_mes2 = UtilsDriver.get_driver().find_element(By.XPATH, xpath2).textprint("生成截图")allure.attach(UtilsDriver.get_driver().get_screenshot_as_png(), "截图",allure.attachment_type.PNG)print("实际mes:" + actual_mes + "预期结果" + expected_msg)Logger.logger_in().info("实际mes:" + actual_mes + ";预期结果" + expected_msg)assert expected_msg in actual_mesprint("1断言成功!")Logger.logger_in().info('1断言成功!')
用于接口自动化的工具类
# utils_api.py,用于接口自动化的工具类
"""
设置驱动
驱动停止
获取弹窗信息
获取data数据
"""
import datetime
import requests
from log.log import Loggerclass RequestUtils:session = requests.session()@classmethod"""定义发送请求的方法,参数为data"""def send_request_data(cls, url, method, data, **kwargs):try:Logger.logger_in().info('-----------------{}接口开始执行-----------------'.format(url))response = RequestUtils.session.request(url=url, method=method, data=data, **kwargs)Logger.logger_in().info('接口请求成功,响应值为:{}'.format(response.text))return responseexcept Exception as e:Logger.logger_in().error('接口请求失败,原因为:{}'.format(repr(e)))return e@classmethod"""定义发送请求的方法,参数为json"""def send_request_json(cls, url, method, data, **kwargs):try:Logger.logger_in().info('-----------------{}接口开始执行-----------------'.format(url))print('-----------------{}接口开始执行-----------------'.format(url))response = RequestUtils.session.request(url=url, method=method, json=data, **kwargs)Logger.logger_in().info('接口请求成功,响应值为:{}'.format(response.text))Logger.logger_in().info('请求体为:{}'.format(response.request.body))print('请求体为:{}'.format(response.request.body))print('接口请求成功,响应值为:{}'.format(response.text))return responseexcept Exception as e:Logger.logger_in().error('接口请求失败,原因为:{}'.format(repr(e)))return e@classmethod"""定义发送请求的方法(get请求,参数为拼接方式),参数为dicts(dicts = {'a': 1, 'b': 2, 'c': 3})"""def send_request_splicing(cls, dicts, url):  # 对应请求的入参及请求的函数Logger.logger_in().info('-----------------{}接口开始执行-----------------'.format(url))print('-----------------{}接口开始执行-----------------'.format(url))def parse_url(data: dict): # 将一个字典(data)转换成一个 URL 查询字符串(query string)item = data.items()urls = "?"for i in item:(key, value) = itemp_str = key + "=" + valueurls = urls + temp_str + "&"urls = urls[:len(urls) - 1]print('请求体为:{}'.format(urls))Logger.logger_in().info('请求体为:{}'.format(urls))return urlsresponse = RequestUtils.session.get(url + parse_url(dicts))Logger.logger_in().info('接口请求成功,响应值为:{}'.format(response.json()))print('接口请求成功,响应值为:{}'.format(response.json()))print(response.json()["data"][0]["a"]) #json串被解析为一个字典,data对应的值是一个列表,列表包含字典,取data列表的第一个字典中a键对应的值return response

base类

用于ui自动化定位元素继承使用

# base_ui.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from utils_app import UtilsDriverclass BaseApp:def __init__(self):print("引用基类:BaseApp")self.driver = UtilsDriver.get_app_driver()  # get_driver方法的引用就有隐形等待# self.driver.implicitly_wait(10)print("已获取app驱动")def get_element(self, location):  # page页对象层的基类,显式等待wait = WebDriverWait(self.driver, 10, 1)element = wait.until(lambda x: x.find_element(location))return elementdef get_elements(self, xpath):""":param xpath: 表示元素定位的路径:return: 返回找到的元素"""wait = WebDriverWait(self.driver, 10, 1)element = wait.until(lambda x: x.find_element(By.XPATH, xpath))return elementdef get_element_id(self, ID):""":param ID::return:"""wait = WebDriverWait(self.driver, 15, 1)element = wait.until(lambda x: x.find_element(By.ID, ID))return elementdef get_element_text(self, XPATH):wait = WebDriverWait(self.driver, 15, 1)element = wait.until(lambda x: x.find_element(By.XPATH, XPATH))return elementdef get_app_element(self, location):wait = WebDriverWait(self.driver, 15, 1)element = wait.until(lambda x: x.find_element(*location))return elementdef get_element_wait(self, location):# page页对象层的基类,显式等待# (定义等待条件,当条件发生时才执行后续代码。程序会轮询查看条件是否发生(默认 10 秒),# 如果条件成立则执行下一步,否则继续等待,直到超过设置的最长时间,程序抛出异常。)# 相较于隐性等待,这个显性等待要明确等待条件和等待上限。比如隐性等待,只要元素存在,可找到就可以,但显性等待,我要明确条件是我的元素可见。而元素存在,并不一定是元素可见。# 显性等待的场景:操作引起了页面的变化,而接下来要操作变化的元素的时候,就需要使用显性等待wait = WebDriverWait(self.driver, 10, 1)element_wait = wait.until(lambda x: x.find_element(By.XPATH, location))return element_waitdef get_switch_to_frame(self, ida):self.driver.implicitly_wait(10)ele_frame = self.driver.find_element(By.ID, ida)return self.driver.switch_to.frame(ele_frame)def get_element_1(self, xpath):""":param xpath: 表示元素定位的路径:return: 返回找到的元素"""self.driver.implicitly_wait(10)return self.driver.find_element(By.XPATH, xpath)class BaseHandle:def input_text(self, element, text):""":param element: 表示元素得对象:param text: 表示要输入的内容:return:"""element.clear()element.send_keys(text)

log类

用于记录所有执行目录

# log.py
import logging
import datetime
import osclass Logger:__logger = None@classmethoddef logger_in(cls):if cls.__logger is None:# 创建日志器cls.__logger = logging.getLogger("APIlogger")cls.__logger.setLevel(logging.DEBUG)# 判断是否存在handler,不然每次都会新建一个handler,导致日志重复输出if not cls.__logger.handlers:# 获取当前日期为文件名,年份最后2位+月份+日期file_name = str(datetime.datetime.now().strftime('%g' + '%m' + "%d")) + '.log'# 创建处理器handler = logging.FileHandler(os.path.join('', file_name))# handler = logging.StreamHandler()# 创建格式器formatter = logging.Formatter('%(asctime)s [%(filename)s:%(lineno)d] %(levelname)s  %(message)s','%Y-%m-%d %H:%M:%S')cls.__logger.addHandler(handler)handler.setFormatter(formatter)return cls.__logger

page类

用于定位UI元素

形成业务用例的执行流程(以登录为例)

# page_ui.py
import timeimport allurefrom utils import UtilsDriver
from base.base_page import BasePage
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
from log.log import Loggerclass PageLogin(BasePage):  # 对象库层def __init__(self):super().__init__()def find_username(self):return self.get_element_1("//*/input[@placeholder='用户名']")def find_password(self):return self.get_element_1("//*/input[@placeholder='密码']")def find_login_bt(self):return self.get_element_1("(//div[contains(text(),'登录')])")class HandleLogin: # 操作层def __init__(self):self.driver = UtilsDriver.get_driver()self.login_page = PageLogin()self.keys = Keys()self.ac = ActionChains(self.driver)def click_and_input_find_username(self, username):  # 点击用户名输入框self.login_page.find_username().click()for i in range(10):self.login_page.find_username().send_keys(Keys.BACK_SPACE)  # 无法使用clear,只能点10次BACK_SPACEself.login_page.find_username().send_keys(username)def click_and_input_find_password(self, password):self.login_page.find_password().click()for i in range(20):self.login_page.find_password().send_keys(Keys.BACK_SPACE)  # 无法使用clear,只能点10次BACK_SPACEself.login_page.find_password().send_keys(password)def click_login_bt(self):self.login_page.find_login_bt().click()class LoginProxy: # 业务层def __init__(self):self.handle_login = HandleLogin()def login(self, username, password, xpath, expected_msg, xpath2, expected_msg2):time.sleep(1)self.handle_login.click_and_input_find_username(username)print("输入用户名")Logger.logger_in().info('输入用户名!')self.handle_login.click_and_input_find_password(password)print("输入密码")Logger.logger_in().info('输入密码!')self.handle_login.click_login_bt()print("点击登录")Logger.logger_in().info('点击登录!')time.sleep(2)UtilsDriver.get_text(xpath, expected_msg, xpath2, expected_msg2)
用于封装请求+断言的方法

会引用到utils_api.py中的方法

# page_api.py
class PageUrl:def __init__(self):self.session = requests.session()def post(self, url, method, data, assert_msg):response = RequestUtils().send_request_json(url, method, data)print("实际response:" + response.text + ";预期响应:" + assert_msg)assert response.text in assert_msgreturn responsedef get(self, url, params, assert_msg):  # 对应请求的断言的函数response = RequestUtils().send_request_splicing(url, params)print("实际response:" + str(response.json()) + ";预期响应:" + assert_msg)print("实际response.json()[‘data’][0][‘a’]:" + response.json()["data"][0]["a"])assert response.json()["data"][0]["a"] == assert_msgreturn response

test文件

接口自动化脚本的入口

# test_api.py
import os
import allure
import time
import sys
import pytest
import hashlib
import urllib.parse
# from page.page_login import LoginProxy
from page_url.page_url import PageUrl
from utils_app import DbMysql
from utils_url_def import RequestUtils  # 有导入就有可能执行
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)class TestUrl:def setup_class(self):  # 实例化page中的业务对象"""pytest中的测试类必须以“Test”开头,且不能有init方法你试下把"Login_test"更改以“Test”开头的命名如果还不行的话文件名更改成“test_”开头或者以“_test”结尾因为pytest命名规范有3点:文件名以“test_”开头或者以“_test”结尾测试类必须以“Test”开头,且不能有init方法测试方法必须以test开头"""self.page_url = PageUrl()self.absolute_xpath = os.path.abspath(os.path.dirname(os.getcwd()))print("当前文件路径:"+BASE_DIR)  # 输入当前的文件路径@pytest.mark.run(order=1)# 执行顺序,数字越小越先执行@allure.title("用例名")  # 命名用例名称方式1@allure.severity(allure.severity_level.CRITICAL)# 关键用例@pytest.mark.skipif(condition=True, reason="暂停")  # 跳过@pytest.mark.parametrize("参数名1,参数名2,参数名3,参数名4,参数名5,参数名6",[(f'{RequestUtils.参数A1}', f'{RequestUtils.参数B1}', 1,  '', '断言1'), (f'{RequestUtils.参数A2}', f'{RequestUtils.参数B2}', 1,  '', '断言2'),]) # 参数化,可用于执行多条用例; ''表参数为空def test_001_entrance(self, A, B, C, D, E, assert_msg):data = {"a": 1,"b": 2}self.page_url.patrol_add_new_001("http:***", "post", data, assert_msg)  time.sleep(2)@pytest.mark.run(order=2) # 执行顺序,数字越小越先执行@allure.title("用例名2")  # 命名用例名称方式1@allure.severity(allure.severity_level.NORMAL)# 正常级别用例# @pytest.mark.skipif(condition=True, reason="暂停")  # 暂停%s,%RequestUtils.test_numberdef test_003_wechat_api(self):dicts = {'a': 1, 'b': 2}url = "http:***"time.sleep(2)self.page_url.wechat_public_account_api(dicts, url, RequestUtils.test_number2)time.sleep(0.3)

UI自动化脚本的入口

# test_ui.py
"""
以模块名称作为测试套件的名称,不要全部堆在一个测试套件里
[pytest]#标识当前配置文件是pytest的配置文件
addopts=-s -v #标识pytest执行的参数
testpaths=./scripts #匹配搜索的目录
python_files=test_*.py #匹配搜索的文件
python_classes=Test* #匹配搜索的类
python_functions=test_* #匹配测试的方法执行结果生成测试报告步骤
1先生成json数据---pytest 测试文件(在pytest.ini在addopts参数后+--alluredir report2)
2再把生成的json生成测试报告---allure generate report/ -o report/html --clean
allure generate report2/ -o report2/html --clean
注意目录的路径及名称
"""
import os
import allure
import time
import datetime
import sys
import pytest
from page.page_login import LoginProxy
from page_url.page_url import PageUrl
from page.page_inside_the_road_in_backstage import InsideTheRoadProxy
from page.page_finance_in_backstage import FinanceProxy
from utils import UtilsDriver
from utils_url_def import RequestUtils
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)class TestLogin:def setup_class(self):  # 实例化page中的业务对象"""pytest中的测试类必须以“Test”开头,且不能有init方法你试下把"Login_test"更改以“Test”开头的命名如果还不行的话文件名更改成“test_”开头或者以“_test”结尾因为pytest命名规范有3点:文件名以“test_”开头或者以“_test”结尾测试类必须以“Test”开头,且不能有init方法测试方法必须以test开头"""self.driver = UtilsDriver.get_driver()self.driver.implicitly_wait(6)self.login_proxy = LoginProxy()self.inside_the_road_proxy = InsideTheRoadProxy()self.finance_proxy = FinanceProxy()self.page_url = PageUrl()self.current_time = str(datetime.datetime.now().strftime("%Y-%m-%d"))self.absolute_xpath = os.path.abspath(os.path.dirname(os.getcwd()))print("当前文件路径:"+BASE_DIR)  # 输入当前的文件路径print(self.current_time)def teardown_class(self):time.sleep(3)UtilsDriver.get_driver().quit()@allure.step(title="正向登录")@allure.title("用例名字")@allure.severity(allure.severity_level.BLOCKER)# 冒烟测试用例@pytest.mark.run(order=4)# @pytest.mark.skipif(condition=True, reason="暂停")  # 跳过该用例def test_001_login(self):self.login_proxy.login(UtilsDriver.user,UtilsDriver.pwd,UtilsDriver.login_actual_xpath,UtilsDriver.login_expected_mes,UtilsDriver.login_actual_xpath,UtilsDriver.login_expected_mes)#在UtilsDriver的方法中订单变量login_expected_mes

main文件

# main.py
import os
import pytest
if __name__ == '__main__':pytest.main()  # 这里已经执行了pytest.ini成为临时文件了(pytest的配置文件自己根据需求配置)os.system("allure generate report/ -o report/html --clean")  # 再次生成测试报告

依赖库

# requirements.txt
allure_python_commons==2.13.5
Appium_Python_Client==2.11.0
Appium_Python_Client==4.0.1
PyMySQL==1.1.0
pytest==8.1.1
Requests==2.32.3
selenium==4.23.1

后言

以上为多端集成联动测试的完全代码,也包含UI自动化和接口自动化的结合,用的是pytest框架,执行后自动生成allure测试报告。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 利用Qt实现调用文字大模型的API,文心一言、通义千问、豆包、GPT、Gemini、Claude。
  • Python Beautiful Soup介绍
  • js逻辑或(||)和且()
  • 在Jira中使用AI
  • 【Mind+】掌控板入门教程02 趣味相框
  • CTFHub——XSS——反射型
  • [YashanDB认证]YashanDB个人版安装
  • ECCV 2024 | 深入探索GAN先验,哈工大、清华提出模型反演新方案
  • 校园课程助手【4】-使用Elasticsearch实现课程检索
  • 2024上海初中生古诗文大会暑假备考:单选题真题和独家解析
  • RAG 的优化进阶与引入 Reranker
  • 频率的工程测量01 - Rif算法的构造
  • 双阈值最大最小值筛选
  • 锂离子电池健康状态预测(Part1,Python)
  • Unity Shader unity文档学习笔记(十八):unity雾效原理
  • 4个实用的微服务测试策略
  • Android交互
  • CentOS学习笔记 - 12. Nginx搭建Centos7.5远程repo
  • Django 博客开发教程 16 - 统计文章阅读量
  • es6
  • Java 网络编程(2):UDP 的使用
  • JS变量作用域
  • Linux编程学习笔记 | Linux IO学习[1] - 文件IO
  • MaxCompute访问TableStore(OTS) 数据
  • mysql innodb 索引使用指南
  • Rancher如何对接Ceph-RBD块存储
  • V4L2视频输入框架概述
  • Vue小说阅读器(仿追书神器)
  • 第三十一到第三十三天:我是精明的小卖家(一)
  • 翻译--Thinking in React
  • 分类模型——Logistics Regression
  • 如何将自己的网站分享到QQ空间,微信,微博等等
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • 云栖大讲堂Java基础入门(三)- 阿里巴巴Java开发手册介绍
  • 怎么把视频里的音乐提取出来
  • 阿里云重庆大学大数据训练营落地分享
  • ​渐进式Web应用PWA的未来
  • #Linux(Source Insight安装及工程建立)
  • #我与Java虚拟机的故事#连载16:打开Java世界大门的钥匙
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (¥1011)-(一千零一拾一元整)输出
  • (2)(2.10) LTM telemetry
  • (void) (_x == _y)的作用
  • (苍穹外卖)day03菜品管理
  • (二十一)devops持续集成开发——使用jenkins的Docker Pipeline插件完成docker项目的pipeline流水线发布
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (附源码)springboot车辆管理系统 毕业设计 031034
  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • (汇总)os模块以及shutil模块对文件的操作
  • (经验分享)作为一名普通本科计算机专业学生,我大学四年到底走了多少弯路
  • (每日持续更新)jdk api之StringBufferInputStream基础、应用、实战
  • (入门自用)--C++--抽象类--多态原理--虚表--1020
  • (四十一)大数据实战——spark的yarn模式生产环境部署
  • (一)ClickHouse 中的 `MaterializedMySQL` 数据库引擎的使用方法、设置、特性和限制。
  • .bat批处理(七):PC端从手机内复制文件到本地