企业微信web自动化测试实战
Web 自动化测试实战
直播前准备
- 重点学习 Page Object 设计模式
专题课 | 阶段 | 教程地址 | 视频地址 |
---|---|---|---|
用户端 Web 自动化测试 | L2 | 显式等待高级使用 | 显式等待高级使用 55:39 |
用户端 Web 自动化测试 | L2 | 自动化关键数据记录 | 自动化关键数据记录 28:24 |
用户端 Web 自动化测试 | L3 | page object 设计模式 | page object 设计模式 17:32 |
用户端 Web 自动化测试 | L3 | 异常自动截图 | 异常自动截图 44:05 |
用户端 Web 自动化测试 | L3 | 测试用例流程设计 | 测试用例流程设计 22:12 |
课程目标
- 掌握 Web 自动化测试框架封装能力
- 掌握 Web 自动化测试框架优化能力
需求说明
- 完成 Web 自动化测试框架搭建
- 在自动化测试框架中编写企业微信添加成员测试用例
- 优化测试框架
- 输出测试报告
实战思路
实战:使用 PO 模式封装测试框架
线性脚本中存在的问题
- 无法适应 UI 频繁变化
- 无法清晰表达业务用例场景
- 大量的样板代码 driver/find/click
解决方案
- 领域模型适配:封装业务实现,实现业务管理
- 提高效率:降低用例维护成本,提高执行效率
- 增强功能:解决已有框架不满足的情况
Page Object 模式简介
PO 模式(Page Object Model)是自动化测试项目开发实践的最佳设计模式之一。
它的主要用途是把一个具体的页面转换成编程语言当中的一个对象,页面特性转化成对象属性,页面操作转换成对象方法。
PO 思想最开始来源于马丁福勒(Marktin Flewer)在 2004 年发表的一篇文章。最初是叫作 Window driver,后来 selenium 沿用这种思想,后来就改成了 POM。
PO 模式的核心思想是通过对界面元素的封装减少冗余代码,同时在后期维护中,若元素定位发生变化,只需要调整页面元素封装的代码,提高测试用例的可维护性、可读性。
PO 模式的优势
- 降低 UI 变化导致的测试用例脆弱性问题
- 让用例清晰明朗,与具体实现无关
PO 模式建模原则
-
属性意义
- 不要暴露页面内部的元素给外部。
- 不需要建模 UI 内的所有元素。
- 方法意义
- 用公共方法代表 UI 所提供的功能。
- 方法应该返回其他的 PageObject 或者返回用于断言的数据。
- 同样的行为不同的结果可以建模为不同的方法。
- 不要在方法内加断言。
企业微信 Web 端 PO 建模
以下为添加成员功能的原型图。
根据原型图可以梳理添加成员的业务逻辑如下:
- 方块代表一个类
- 每条线代表这个页面提供的方法
- 箭头的始端为开始页面
- 箭头的末端为跳转页面或需要断言的数据
页面元素和服务梳理
清楚业务逻辑后,即可梳理出每个页面包含和元素和需要提供的服务。
项目结构
项目的整个结构如下图所示。
Hogwarts $ tree
.
├── __init__.py
├── base
│ ├── __init__.py
│ └── base_page.py
├── tests
│ ├── __init__.py
│ └── test_xxx.py
├── log
│ ├── test.txt
├── datas
│ └── xxx.yaml
├── page
│ ├── __init__.py
│ ├── main_page.py
│ ├── xxx_page.py
│ └── xxx_page.py
└── utils
├── __init__.py
└── log_utils.py
代码实现
# 代码详见仓库
课堂练习
- 完成空架子搭建
- 运行用例成功即可
实战:填充测试框架
代码实现
# 代码详见仓库
课堂练习
- 拆分添加联系人测试用例到框架中
实战:优化测试框架
基类中封装常用方法
base_page.py
class BasePage:
def __init__(self, driver: WebDriver=None):
if driver == None:
# 初始化 driver
service = Service(executable_path=ChromeDriverManager().install())
self.driver = webdriver.Chrome(service=service)
# 设置隐式等待
self.driver.implicitly_wait(15)
# 最大化窗口
self.driver.maximize_window()
else:
self.driver = driver
def close_browser(self):
'''
关闭浏览器
:return:
'''
self.driver.quit()
def open_url(self, url):
'''
打开网页
:param url: 要打开页面的 url
:return:
'''
self.driver.get(url)
def find_ele(self, by, value):
'''
查找单个元素
:param by: 元素定位方式
:param value: 元素定位表达式
:return: 找到的元素对象
'''
ele = self.driver.find_element(by, value)
return ele
def find_eles(self, by, value):
'''
查找多个元素
:param by: 元素定位方式
:param value: 元素定位表达式
:return: 元素列表
'''
eles = self.driver.find_elements(by, value)
return eles
def find_and_get_text(self, by, value):
'''
获取单个元素的文本属性
:param by: 元素定位方式
:param value: 元素定位表达式
:return: 文本内容
'''
text_value = self.find_ele(by, value).text
return text_value
def click_ele(self, by, value):
'''
查找单个元素并点击
:param by: 元素定位方式
:param value: 元素定位表达式
'''
self.find_ele(by, value).click()
def ele_sendkeys(self, by, value, text):
'''
单个元素输入内容
:param by: 元素定位方式
:param value: 元素定位表达式
:param text: 要输入的内容字符串
'''
# 清除内容
self.find_ele(by, value).clear()
# 输入内容
self.find_ele(by, value).send_keys(text)
def wait_ele_located(self, by, value, timetout=10):
'''
显式等待元素可以被定位
:param by: 元素定位方式
:param value: 元素定位表达式
:param timetout: 等待时间
:return: 定位到的元素对象
'''
ele = WebDriverWait(self.driver, timetout).until(
expected_conditions.invisibility_of_element_located((by, value))
)
return ele
def wait_ele_click(self, by, value, timeout=10):
'''
显式等待元素可以被点击
:param by: 元素定位方式
:param value: 元素定位表达式
:param timeout: 等待时间
'''
ele = WebDriverWait(self.driver, timeout).until(
expected_conditions.element_to_be_clickable((by, value))
)
return ele
def login_by_cookie(self):
'''
通过 cookie 登录
:return:
'''
# 从文件中获取 cookie 信息登陆
with open("../datas/cookie.yaml", "r", encoding="utf-8") as f:
cookies = yaml.safe_load(f)
print(f"读取出来的cookie:{cookies}")
for cookie in cookies:
# 添加 cookie
self.driver.add_cookie(cookie)
# 刷新页面
self.driver.refresh()
定义好之后,其他 page 中调用基类中封装好的方法即可。
课堂练习
- 完成基类中常用方法的封装
添加日志
utils 包下创建单独的日志文件。
import logging
import os
from logging.handlers import RotatingFileHandler
# 绑定绑定句柄到logger对象
logger = logging.getLogger(__name__)
# 获取当前工具文件所在的路径
root_path = os.path.dirname(os.path.abspath(__file__))
# 拼接当前要输出日志的路径
log_dir_path = os.sep.join([root_path, '..', f'/logs'])
if not os.path.isdir(log_dir_path):
os.mkdir(log_dir_path)
# 创建日志记录器,指明日志保存路径,每个日志的大小,保存日志的上限
file_log_handler = RotatingFileHandler(os.sep.join([log_dir_path, 'log.txt']), maxBytes=1024 * 1024, backupCount=10 , encoding="utf-8")
# 设置日志的格式
date_string = '%Y-%m-%d %H:%M:%S'
formatter = logging.Formatter(
'[%(asctime)s] [%(levelname)s] [%(filename)s]/[line: %(lineno)d]/[%(funcName)s] %(message)s ', date_string)
# 日志输出到控制台的句柄
stream_handler = logging.StreamHandler()
# 将日志记录器指定日志的格式
file_log_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)
# 为全局的日志工具对象添加日志记录器
# 绑定绑定句柄到logger对象
logger.addHandler(stream_handler)
logger.addHandler(file_log_handler)
# 设置日志输出级别
logger.setLevel(level=logging.INFO)
其他页面导入 logger 即可定义不同级别的日志。
class BasePage:
def find_ele(self, by, value):
'''
查找单个元素
:param by: 元素定位方式
:param value: 元素定位表达式
:return: 找到的元素对象
'''
logger.info(f"定位单个元素,定位方式为 {by}, 定位表达式为 {value}")
报错截图并保存 PageSource
在基类中添加截图和保存 page_source 的方法。
class BasePage:
def get_path(self, path_name):
'''
获取绝对路径
:param path_name: 目录名称
:return: 目录绝对路径
'''
# 获取当前工具文件所在的路径
root_path = os.path.dirname(os.path.abspath(__file__))
# 拼接当前要输出日志的路径
dir_path = os.sep.join([root_path, '..', f'/{path_name}'])
return dir_path
def screen_image(self):
'''
截图
:return: 图片保存路径
'''
# 截图命名
now_time = time.strftime('%Y_%m_%d_%H_%M_%S')
image_name = f"{now_time}.png"
# 拼接截图保存路径
# windows f"{self.get_path('image')}\\{image_name}"
image_path = f"{self.get_path('image')}/{image_name}"
logger.info(f"截图保存路径为 {image_path}")
# 截图
self.driver.save_screenshot(image_path)
return image_path
def save_page_source(self):
'''
保存页面源码
:return: 页面源码保存路径
'''
# 文件命名
now_time = time.strftime('%Y_%m_%d_%H_%M_%S')
pagesource_name = f"{now_time}_pagesource.html"
# 拼接文件保存路径
# windows f"{self.get_path('pagesource')}\\{pagesource_name}"
pagesource_path = f"{self.get_path('pagesource')}/{pagesource_name}"
logger.info(f"页面源码文件保存路径为 {pagesource_path}")
# 保存 page source
with open(pagesource_path, "w", encoding="utf-8") as f:
f.write(self.driver.page_source)
return pagesource_path
然后可以写错一个元素的定位,运行用例查看执行效果。
课堂练习
- 完成日志添加
- 完成定位不到元素场景下截图与源码保存
实战:生成测试报告
添加 allure 步骤描述
在基类中封装的底层方法中添加 allure 步骤描述。
class BasePage:
def find_ele(self, by, value):
'''
查找单个元素
:param by: 元素定位方式
:param value: 元素定位表达式
:return: 找到的元素对象
'''
info_text = f"定位单个元素,定位方式为 {by}, 定位表达式为 {value}"
logger.info(info_text)
with allure.step(info_text):
try:
ele = self.driver.find_element(by, value)
except Exception as e:
ele = None
logger.info(f"单个元素没有找到 {e}")
# 截图
self.screen_image()
# 保存日志
self.save_page_source()
return ele
allure 报告中添加文件
base_page.py 文件中,在截图和保存page_source 方法中添加文件到 allure。
class BasePage:
def get_path(self, path_name):
'''
获取绝对路径
:param path_name: 目录名称
:return: 目录绝对路径
'''
# 获取当前工具文件所在的路径
root_path = os.path.dirname(os.path.abspath(__file__))
# 拼接当前要输出日志的路径
dir_path = os.sep.join([root_path, '..', f'/{path_name}'])
return dir_path
def screen_image(self):
'''
截图
:return: 图片保存路径
'''
# 截图命名
now_time = time.strftime('%Y_%m_%d_%H_%M_%S')
image_name = f"{now_time}.png"
# 拼接截图保存路径
# windows f"{self.get_path('image')}\\{image_name}"
image_path = f"{self.get_path('image')}/{image_name}"
logger.info(f"截图保存路径为 {image_path}")
# 截图
self.driver.save_screenshot(image_path)
# 添加截图到 allure
allure.attach.file(image_path, name="查找元素异常截图",
attachment_type=allure.attachment_type.PNG)
return image_path
def save_page_source(self):
'''
保存页面源码
:return: 页面源码保存路径
'''
# 文件命名
now_time = time.strftime('%Y_%m_%d_%H_%M_%S')
pagesource_name = f"{now_time}_pagesource.html"
# 拼接文件保存路径
# windows f"{self.get_path('pagesource')}\\{pagesource_name}"
pagesource_path = f"{self.get_path('pagesource')}/{pagesource_name}"
logger.info(f"页面源码文件保存路径为 {pagesource_path}")
# 保存 page source
with open(pagesource_path, "w", encoding="utf-8") as f:
f.write(self.driver.page_source)
# pagesource 添加到 allure 报告
allure.attach.file(pagesource_path, name="查找元素异常的页面源码",
attachment_type=allure.attachment_type.TEXT)
return pagesource_path
测试用例中添加 allure 描述
@allure.feature("企业微信 Web 端")
class TestAddMember:
@allure.story("添加成员")
@allure.title("添加成员-冒烟用例")
def test_add_member(self):
'''
添加成员冒烟用例
:return:
'''
执行命令生成报告
pytest -v --alluredir=./results --clean-alluredir
allure serve ./results
allure generate --clean alluredir results -o results/html
课堂练习
- 运行测试框架,生成测试报告
总结
- PO 模式封装 Web 自动化测试框架
- 框架优化
- 报错保存日志
- 报错截图并保存 PageSource
- 添加测试报告描述