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

软件测试经理工作日常随记【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测试报告。

相关推荐
Feng.Lee几秒前
敏捷测试的特点与价值
功能测试·集成测试·可用性测试
comli_cn11 分钟前
使用清华源安装python包
开发语言·python
赵谨言21 分钟前
基于python 微信小程序的医院就诊小程序
经验分享·python·毕业设计
1.01^100036 分钟前
[1111].集成开发工具Pycharm安装与使用
python·pycharm
HEX9CF37 分钟前
【Django】测试带有 CSRF 验证的 POST 表单 API 报错:Forbidden (CSRF cookie not set.)
python·django·csrf
凡人的AI工具箱1 小时前
每天40分玩转Django:实操多语言博客
人工智能·后端·python·django·sqlite
Py办公羊大侠1 小时前
Excel批量设置行高,Excel表格设置自动换行后打印显示不全,Excel表格设置最合适的行高后打印显示不全,完美解决方案!!!
python·excel·打印·openpyxl·自动换行·显示不全
PieroPc2 小时前
Python tkinter写的《电脑装配单》和 Html版 可打印 可导出 excel 文件
python·html·电脑
Cachel wood2 小时前
Django REST framework (DRF)中的api_view和APIView权限控制
javascript·vue.js·后端·python·ui·django·前端框架
暮色尽染2 小时前
Python 正则表达式
开发语言·python