当前位置:   article > 正文

【UI自动化】Python + Selenium搭建Web端UI自动化框架(下)_python webui自动化

python webui自动化

本文主要讲Web端UI自动化的详细搭建和配置过程,POM设计模式
支持失败截图,日志打印

依赖库:

  • Selenium
  • pytest
  • logging

驱动下载地址: http://chromedriver.storage.googleapis.com/index.html

一、一个完整的UI自动化脚本

1、下载驱动

  • 找到自己浏览器的版本号
    在这里插入图片描述在这里插入图片描述
    此处可以看到我的浏览器版本是 102.0.5005.63(正式版本) (64 位)

驱动下载: http://chromedriver.storage.googleapis.com/index.html

在这里插入图片描述

我们只需要找到最近的一个驱动号就行,不强求非得完全一致

在这里插入图片描述

2、完整脚本

以打开百度网站搜索并点开第一个链接

# 展示Selenium的基本使用

# 导入必要的依赖
import time
from selenium import webdriver
from selenium.webdriver.common.by import By


def demo_case():
    # 加载驱动
    drive = webdriver.Chrome("../lib/win32_chromedriver.exe")

    # 全屏显示
    drive.maximize_window()

    # 打开指定网站
    drive.get("https://www.baidu.com/")

    # 获取指定元素
    input_view = drive.find_element(By.ID,"kw")
    submit_btn = drive.find_element(By.ID,"su")

    # 输入指定内容
    input_view.send_keys("selenium4的一些常用方法")
    # 点击进行搜索
    submit_btn.click()

    time.sleep(4)   # 为了观测效果,稍等卡一下

    # 最后驱动需要关闭
    drive.quit()

if __name__ == '__main__':
    demo_case()
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

二、POM设计模式

根据上一段落的完整脚本的实践,我们可以知道,进行UI自动化无非就是几个步骤

  • 找到元素
  • 操作元素 - 即形成动作。如:按钮为点击,文本框元素为输入和清除内容
  • 编排动作 - 即形成一个用例。如:将输入账号,点击登录按钮这一列组合,就形成了一个登录的用例

那么,根据以上的分析,我们将上边简单的脚本拆分一下,分包。

拆包的意义是什么呢?解耦
就是如果页面元素发生了部分改变,我们可以通过改动最少的代码来更新我们的自动化用例

在这里插入图片描述

  • common
  • lib
    • 存放浏览器驱动
  • Outputs
    • 输出日志信息位置
    • 输出截图位置
    • 输出测试报告的位置
  • pagelocators:元素定位 包
  • pageobj:页面元素操作 包
  • testcase:测试用例 包
  • testdata:测试数据
  • util:工具类
  • sample:案例,各种操作案例包

1、通用基础包

1)common包:dir_config文件

'''
@Author : qiaosirong
@Date   : 2022/6/21 16:48
@Desc   : 目录相关配置,将所有路径进行归拢
'''

import os

# 框架项目顶层目录
base_dir = os.path.split(os.path.split(os.path.abspath(__file__))[0])[0]

testdata_dir = os.path.join(base_dir,'testdata')
testcase_dir = os.path.join(base_dir,'testcase')
lib_dir = os.path.join(base_dir,'lib')
# drive_dir = os.path.join(lib_dir,'102_0_5005_61')   # 驱动所在的文件夹
drive_dir = os.path.join(lib_dir,'win32_chromedriver.exe')   # 驱动所在的文件夹

htmlreport_dir = os.path.join(base_dir,"Outputs/reports")
logs_dir = os.path.join(base_dir,"Outputs/logs")
screenshot_dir = os.path.join(base_dir,"Outputs/screenshots")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2)common包:basepage文件

提供常见的所有元素的通用操作

'''
@Author : qiaosirong
@Date   : 2022/6/21 17:02
@Desc   : 页面的一些基础配置
'''
from selenium.webdriver import ActionChains
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

from common.dir_config import screenshot_dir    # 单独导入截屏地址
from util.diy_log import diy_logger   # 单例模式导入日志类

import time
import datetime

class BasePage:
    """
    # 包含了PageObjects当中,用到所有的selenium底层方法。
    # 还可以包含通用的一些元素操作,如alert,iframe,windows...
    # 还可以自己额外封装一些web相关的断言
    # 实现日志记录、实现失败截图
    """

    def __init__(self,driver:WebDriver):
        self.driver = driver
        self.driver.maximize_window()

    # 等待元素可见
    def wait_ele_visible(self,locator,img_doc,timeout=30,poll_fre=0.5):
        """
        :param locator: 元组类型。(元素定位策略,元素定位表达式)
        :param img_doc: 截图文件的命名部分。${页面名称_行为名称}_当前的时间.png
        :param timeout:
        :param poll_fre:
        :return: None
        """
        diy_logger.info("{} : 等待 {} 元素可见".format(img_doc,locator))
        try:
            # 起始等待的时间 datetime
            start = datetime.datetime.now()
            WebDriverWait(self.driver,timeout,poll_fre).until(EC.visibility_of_element_located(locator))
        except:
            # 异常信息写入日志
            diy_logger.exception("等待元素可见失败:")  # 级别:Error   tracebak的信息完整的写入日志。
            # 截图 - 命名。 页面名称_行为名称_当前的时间.png
            self.save_page_screenshot(img_doc)
            raise
        else:
            # 结束等待的时间
            end = datetime.datetime.now()
            diy_logger.info("等待结束.开始时间为{},结束时间为:{},一共等待耗时为:{}".format(start,end,end-start))

    # 等待元素存在
    def wait_page_contains_element(self,locator,img_doc,timeout=30,poll_fre=0.5):
        diy_logger.info("{} : 等待 {} 元素存在".format(img_doc,locator))
        try:
            # 起始等待的时间 datetime
            start = datetime.datetime.now()
            WebDriverWait(self.driver, timeout, poll_fre).until(EC.presence_of_element_located(locator))
        except:
            # 异常信息写入日志
            diy_logger.exception("等待元素存在失败:")  # 级别:Error   tracebak的信息完整的写入日志。
            # 截图 - 命名。 页面名称_行为名称_当前的时间.png
            self.save_page_screenshot(img_doc)
            raise
        else:
            # 结束等待的时间
            end = datetime.datetime.now()
            diy_logger.info("等待结束.开始时间为{},结束时间为:{},一共等待耗时为:{}".format(start,end,end-start))

    # 查找单个元素
    def get_element(self,locator,img_doc):
        diy_logger.info("{} : 查找 {} 元素.".format(img_doc,locator))
        try:
            ele = self.driver.find_element(*locator)
        except:
            # 异常信息写入日志
            diy_logger.exception("查找元素失败:")  # 级别:Error   tracebak的信息完整的写入日志。
            # 截图 - 命名。 页面名称_行为名称_当前的时间.png
            self.save_page_screenshot(img_doc)
            raise
        else:
            return ele

    def input_text(self,locator,value,img_doc,timeout=30,poll_fre=0.5):
        # 1)等待元素可见;2)查找元素;3)输入动作
        self.wait_ele_visible(locator,img_doc,timeout,poll_fre)
        ele = self.get_element(locator,img_doc)
        diy_logger.info("{}: 对 {} 元素输入文本 {}".format(img_doc,locator,value))
        try:
            ele.send_keys(value)
        except:
            # 异常信息写入日志
            diy_logger.exception("输入文本失败:")  # 级别:Error   tracebak的信息完整的写入日志。
            # 截图 - 命名。 页面名称_行为名称_当前的时间.png
            self.save_page_screenshot(img_doc)
            raise

    def click_element(self,locator,img_doc,timeout=30,poll_fre=0.5):
        # 1)等待元素可见;2)查找元素;3)点击
        self.wait_ele_visible(locator, img_doc, timeout, poll_fre)
        ele = self.get_element(locator, img_doc)
        diy_logger.info("{}: 点击 {} 元素 ".format(img_doc,locator))
        try:
            ele.click()
        except:
            # 异常信息写入日志
            diy_logger.exception("点击操作失败:")  # 级别:Error   tracebak的信息完整的写入日志。
            # 截图 - 命名。 页面名称_行为名称_当前的时间.png
            self.save_page_screenshot(img_doc)
            raise

    def get_element_text(self,locator,img_doc,timeout=30,poll_fre=0.5):
        # 1)等待元素存在;2)查找元素;3)获取动作
        self.wait_ele_visible(locator,img_doc,timeout,poll_fre)
        ele = self.get_element(locator,img_doc)
        diy_logger.info("{}: 获取 {}  元素的文本内容.".format(img_doc,locator))
        try:
            text = ele.text
        except:
            # 异常信息写入日志
            diy_logger.exception("获取元素文本值失败:")  # 级别:Error   tracebak的信息完整的写入日志。
            # 截图 - 命名。 页面名称_行为名称_当前的时间.png
            self.save_page_screenshot(img_doc)
            raise
        else:
            diy_logger.info("获取的文本值为: {}".format(text))
            return text

    def get_element_attribute(self,locator,attr,img_doc,timeout=30,poll_fre=0.5):
        # 1)等待元素存在;2)查找元素;3)获取动作
        self.wait_page_contains_element(locator, img_doc, timeout, poll_fre)
        ele = self.get_element(locator, img_doc)
        diy_logger.info("{}: 获取 {}  元素的属性 {}.".format(img_doc,locator,attr))
        try:
            value = ele.get_attribute(attr)
        except:
            # 异常信息写入日志
            diy_logger.exception("获取元素属性失败:")  # 级别:Error   tracebak的信息完整的写入日志。
            # 截图 - 命名。 页面名称_行为名称_当前的时间.png
            self.save_page_screenshot(img_doc)
            raise
        else:
            diy_logger.info("获取的属性值为: {}".format(value))
            return value

    def check_element_visible(self,locator,img_doc,timeout=10,poll_fre=0.5):
        """
         # 检测元素是否在页面存在且可见。
         如果退出元素存在,则返回True。否则返回False
        :return: 布尔值
        """
        diy_logger.info("{}: 检测元素 {} 存在且可见于页面。".format(img_doc,locator))
        try:
            WebDriverWait(self.driver,timeout,poll_fre).until(EC.visibility_of_element_located(locator))
        except:
            diy_logger.exception(" {}秒内元素在当前页面不可见。".format(timeout))
            self.save_page_screenshot(img_doc)
            return False
        else:
            diy_logger.info(" {}秒内元素可见。".format(timeout))
            return True

    def check_text_visible(self,text,img_doc,timeout=10,poll_fre=0.5):
        """
         # 检测文本是否在页面存在且可见。
         如果检测出输入的文本 存在,则返回True。否则返回False
        :return: 布尔值
        """
        diy_logger.info("{}: 检测文本- {} -存在且可见于页面。".format(img_doc,text))
        if text in self.driver.page_source:
            return True
        return False

    def switch_window(self):
        pass

    def get_current_url(self):
        pass

    def save_page_screenshot(self,img_doc):
        """
        :param img_doc:
        :return:
        """
        # 路径配置文件中引入图片保存路径  + 年月日-时分秒
        #  # 截图 - 命名。 页面名称_行为名称_当前的时间.png
        #  页面_功能_时间.png
        now = time.strftime("%Y-%m-%d %H_%M_%S")
        screenshot_path = screenshot_dir + "/{}_{}.png".format(img_doc, now)
        try:
            self.driver.save_screenshot(screenshot_path)
        except:
            diy_logger.exception("当前网页截图失败")
        else:
            diy_logger.info("截取当前网页成功并存储在: {}".format(screenshot_path))

    def move_login_slider(self, locator, img_doc, move_len=294, timeout=30, poll_fre=0.5):
        self.wait_ele_visible(locator, img_doc, timeout, poll_fre)  # 元素等待时间
        action = ActionChains(self.driver)
        ele = self.get_element(locator, img_doc)    # 获取滑块元素
        # 按下滑块
        action.click_and_hold(ele)
        # 滑动一定的偏移量(通过元素检查查看)
        action.move_by_offset(move_len, 0)
        # 释放鼠标
        action.release()
        # 执行上述action操作
        action.perform()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210

3)util:日志类

'''
@Author : qiaosirong
@Date   : 2022/6/21 11:46
@Desc   : 自定义logger日志格式
'''

import logging.handlers

import time

from common import dir_config


class DuLogger(object):
    def __init__(self):
        self.logger = logging.getLogger('qsr_logger')
        # 设置输出的日志级别为debug级别以上
        self.logger.setLevel(level=logging.DEBUG)

        # 同步输出在控制台
        handler_std = logging.StreamHandler()

        curTime = time.strftime("%Y-%m-%d %H%M", time.localtime())
        # 定义处理器
        file_handle = logging.handlers.TimedRotatingFileHandler(
            dir_config.logs_dir + "/QSRDemo_Web_Autotest_{0}.log".format(curTime),
            backupCount=0,
            encoding='utf-8',
            when='D',           # when参数可以设置S M H D,分别是秒、分、小时、天分割,也可以按周几分割,也可以凌晨分割
            interval=1)
        file_handle.setLevel(level=logging.INFO)
        file_handle.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s'))


        self.logger.addHandler(file_handle)
        self.logger.addHandler(handler_std)

diy_logger = DuLogger().logger

def testOne():
    print()
    diy_logger.info("info ..")
    diy_logger.debug("debug ..")
    diy_logger.warning("warning ..")
    diy_logger.error("error ..")
    diy_logger.critical("critical ..")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

4)Pytest的conftest编写

为了一些全局的方法,公用的,如驱动加载和关闭
可以延展为:打开某一个指定的系统,并且直接完成登录等相关基础操作

# 测试用例级别

import pytest
from selenium import webdriver
from util.diy_log import diy_logger
from common.dir_config import drive_dir
@pytest.fixture
def init_driver_function():
    """
    前置:打开浏览器,访问系统网址,去除浏览器特征值
    后置:退出浏览器。
    """
    diy_logger.info("***** conftest.py共享的 init_driver 的前置  *****")
    driver = webdriver.Chrome(drive_dir + '/chromedriver_mac_M1')
    driver.get("www.baidu.com")     # 此处一样可以提取
    yield driver
    driver.quit()
    diy_logger.info("***** conftest.py共享的 init_driver 的后置  *****")

"""
init_driver的前置
init_login的前置
init_login的后置
init_driver的后置

假设:init_driver是class级,init_login是function级别?
      可以"继承"。
      init_driver是function级,init_login是function级别?
      可以调用。
      init_driver是function级,init_login是class级别?
      不可以。

一个fixture,可以使用比它高的/与它同级的 fixture作为它的参数。 

   function,可以调用class,function,module,session.
   function最小单位。最后执行。其它的级别一定是比它先执行。

"""

@pytest.fixture(scope="session",autouse=True)
def mySs():
    print("**** 我是session级别的fixture 前置 ****")
    yield
    print("**** 我是session级别的fixture 后置 ****")


@pytest.fixture(scope="module")
def myMo():
    print("**** 我是module级别的fixture 前置 ****")
    yield
    print("**** 我是module级别的fixture 后置 ****")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

2、pagelocators:“元素定位”包

"元素定位"包,就是一个个控件的坐标,当这些位置或xpath等信息发生变化时,我们只需要修改这里就行,其他地方不再动。

"""
@Author :   qiaosirong
@Date   :   2022/8/30 22:09
@Desc   :   百度首页的元素定位
"""
from selenium.webdriver.common.by import By

class BaiduPageLoc(object):
    # 输入框
    input_view_loc = (By.ID, "kw")
    # 确认按钮
    submit_btn = (By.ID, "su")
    # logo的位置
    logo_loc = (By.ID,"s_lg_img")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3、pageobj:“页面动作”包

"""
@Author :   qiaosirong
@Date   :   2022/8/30 22:13
@Desc   :   百度首页的页面元素动作
"""
from pagelocators.baidu_loc import BaiduPageLoc as loc
from common.basepage import BasePage

class BaiduPage(BasePage):

    # 进行搜索操作
    def searchTar(self, search_data):
        self.input_text(loc.input_view_loc, search_data, "这个是如果输入失败的截图名称")
        self.click_element(loc.submit_btn, "这个是如果点击失败的截图名称")

    # 点击logo的操作
    def click_logo(self):
        self.click_element(loc.logo_loc, "点击logo点击错了的截图")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

4、testdata:测试数据包

用于搜索内容的search_data文件

# 成功用例
success_data = {"mes1": "aaaa"}

# ”失败“的测试数据,一个花括号是一组
wrong_datas = [
    {"mes1": "111"},
    {"mes1": "222"},
    {"mes1": "333"}
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

5、testcase:测试用例

将动作汇总到一起,利用pytest的yield特点,实现前置和后置方法的合并

'''
@Author : qiaosirong
@Date   : 2022/6/21 19:16
@Desc   : 商家工作台登录页面的测试case
'''
from time import sleep

import pytest
from util.diy_log import diy_logger   # 单例模式导入日志类
from pageobj.baidu_page import BaiduPage
from testdata import search_data as td
from urllib.parse import urlparse

@pytest.fixture
def init(init_driver_function):
    diy_logger.info("***** TestBaidu用例自己的 init 的前置  *****")
    baidu_page = BaiduPage(init_driver_function)
    yield init_driver_function,baidu_page   # (driver对象,lp页面对象)
    diy_logger.info("***** TestBaidu用例自己的 init 的后置  *****")

@pytest.mark.usefixtures("init")
class TestFrontLogin():
    # 点击百度的logo
    def test_baidu_click_logo(self,init):
        diy_logger.info("******* 点击Baidu logo *******")
        # 点击百度首页的logo。
        init[1].click_logo()
        sleep(1)
        # 断言
        assert True


    @pytest.mark.parametrize("case", td.wrong_datas)
    def test_login_success(self,case,init):
        diy_logger.info("******* 使用成功的用例进行测试 *******")
        # 调用登陆页面的。登陆行为。
        init[1].searchTar(case["mes1"])
        sleep(1)
        assert True
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

三、运行日志

在这里插入图片描述
失败的截图
在这里插入图片描述

四、可继续改进点

  • 测试数据存入数据库
  • 测试结果入库,做定制化
  • 将元素的定位信息进行持久化,提供可视化页面进行动态编辑和配置
  • 提供api接口自动启动测试,用于CI/CD自动触发和手动调用触发
  • 与Appium进行集成
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/94323
推荐阅读
相关标签
  

闽ICP备14008679号