软件测试经理工作日常随记【7】-UI自动化(多端集成测试)
前言
今天开这篇的契机是,最近刚好是运维开发频繁更新证书的,每次更新都在0点,每次一更新都要走一次冒烟流程。为了不让我的美容觉被阉割!(bushi)为了方便同事儿,不用每次更新都求爷告奶地通知大家辛苦半夜走一遍测试。我紧赶慢赶,于是有了此。因为此次冒烟涉及三个端,其中两个端采用接口自动化,另外一个端采用UI自动化,集成运行。
正文
工具类
用于UI自动化的工具类
python
# utils.py 用于UI自动化的工具类,包含在pc端有界面的执行和linux服务器无界面的执行(本段代码以linux服务器无界面运行为例)
"""
设置驱动
驱动停止
获取弹窗信息
获取data数据
"""
import json
import time
import 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 Logger
class UtilsDriver:
@classmethod
def 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_path
cls._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
@classmethod
def 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
@classmethod
def 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.text
alist.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
@classmethod
def get_elements(cls, xpath):
"""
:param xpath: 表示元素定位的路径
:return: 返回找到的元素
"""
return UtilsDriver.get_driver().find_elements(By.XPATH, xpath)
@classmethod
def get_attribute(cls, xpath, attribute):
"""
以元素的属性值来断言,断言前必须延迟2s
:param xpath: 找到元素的路径,只取到前面的标签,然后根据标签中的元素名来判断属性值对不对
:param attribute: 标签中的元素名
:return: 属性值
"""
return UtilsDriver.get_driver().find_element(By.XPATH, xpath).get_attribute(attribute)
@classmethod
def 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).text
actual_mes2 = UtilsDriver.get_driver().find_element(By.XPATH, xpath2).text
print("生成截图")
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_mes
print("1断言成功!")
Logger.logger_in().info('1断言成功!')
assert expected_msg2 in actual_mes2
print("2断言成功!")
Logger.logger_in().info('2断言成功!')
@classmethod
def 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).text
print("生成截图")
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_mes
print("1断言成功!")
Logger.logger_in().info('1断言成功!')
用于接口自动化的工具类
python
# utils_api.py,用于接口自动化的工具类
"""
设置驱动
驱动停止
获取弹窗信息
获取data数据
"""
import datetime
import requests
from log.log import Logger
class 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 response
except 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 response
except 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) = i
temp_str = key + "=" + value
urls = urls + temp_str + "&"
urls = urls[:len(urls) - 1]
print('请求体为:{}'.format(urls))
Logger.logger_in().info('请求体为:{}'.format(urls))
return urls
response = 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自动化定位元素继承使用
python
# base_ui.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from utils_app import UtilsDriver
class 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 element
def 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 element
def 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 element
def get_element_text(self, XPATH):
wait = WebDriverWait(self.driver, 15, 1)
element = wait.until(lambda x: x.find_element(By.XPATH, XPATH))
return element
def get_app_element(self, location):
wait = WebDriverWait(self.driver, 15, 1)
element = wait.until(lambda x: x.find_element(*location))
return element
def 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_wait
def 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类
用于记录所有执行目录
python
# log.py
import logging
import datetime
import os
class Logger:
__logger = None
@classmethod
def 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元素
形成业务用例的执行流程(以登录为例)
python
# page_ui.py
import time
import allure
from 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 Logger
class 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_SPACE
self.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_SPACE
self.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中的方法
python
# 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_msg
return response
def 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_msg
return response
test文件
接口自动化脚本的入口
python
# 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_number
def 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自动化脚本的入口
python
# 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文件
python
# main.py
import os
import pytest
if __name__ == '__main__':
pytest.main() # 这里已经执行了pytest.ini成为临时文件了(pytest的配置文件自己根据需求配置)
os.system("allure generate report/ -o report/html --clean") # 再次生成测试报告
依赖库
typescript
# 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测试报告。