阅读量:0
软件测试经理工作日常随记【7】-UI自动化(多端集成测试)
前言
今天开这篇的契机是,最近刚好是运维开发频繁更新证书的,每次更新都在0点,每次一更新都要走一次冒烟流程。为了不让我的美容觉被阉割!(bushi)为了方便同事儿,不用每次更新都求爷告奶地通知大家辛苦半夜走一遍测试。我紧赶慢赶,于是有了此。因为此次冒烟涉及三个端,其中两个端采用接口自动化,另外一个端采用UI自动化,集成运行。
正文
工具类
用于UI自动化的工具类
# 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断言成功!')
用于接口自动化的工具类
# 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自动化定位元素继承使用
# 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类
用于记录所有执行目录
# 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元素
形成业务用例的执行流程(以登录为例)
# 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中的方法
# 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文件
接口自动化脚本的入口
# 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自动化脚本的入口
# 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测试报告。