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

avatar
作者
筋斗云
阅读量: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测试报告。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!