赞
踩
本文主要讲Web端UI自动化的详细搭建和配置过程,POM设计模式
支持失败截图,日志打印
依赖库:
- Selenium
- pytest
- logging
驱动下载地址: http://chromedriver.storage.googleapis.com/index.html
102.0.5005.63(正式版本) (64 位)
我们只需要找到最近的一个驱动号就行,不强求非得完全一致
以打开百度网站搜索并点开第一个链接
# 展示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()
根据上一段落的完整脚本的实践,我们可以知道,进行UI自动化无非就是几个步骤
那么,根据以上的分析,我们将上边简单的脚本拆分一下,分包。
拆包的意义是什么呢?解耦
就是如果页面元素发生了部分改变,我们可以通过改动最少的代码来更新我们的自动化用例
'''
@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")
提供常见的所有元素的通用操作
'''
@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()
'''
@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 ..")
为了一些全局的方法,公用的,如驱动加载和关闭
可以延展为:打开某一个指定的系统,并且直接完成登录等相关基础操作
# 测试用例级别
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 后置 ****")
"元素定位
"包,就是一个个控件的坐标,当这些位置或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")
"""
@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点击错了的截图")
用于搜索内容的search_data文件
# 成功用例
success_data = {"mes1": "aaaa"}
# ”失败“的测试数据,一个花括号是一组
wrong_datas = [
{"mes1": "111"},
{"mes1": "222"},
{"mes1": "333"}
]
将动作汇总到一起,利用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
失败的截图
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。