【线上】web自动化测试实战
Web 自动化测试实战
直播前准备
- 重点学习 Page Object 设计模式
注意: Java技术栈的同学选择Java班级,Python技术栈的同学选择Python班级。
专题课 | 阶段 | 章节 | Python 班级 | Java 班级 |
---|---|---|---|---|
用户端 Web 自动化测试 | L2 | 显式等待高级使用 | Python版录播 | Java版录播 |
用户端 Web 自动化测试 | L2 | 自动化关键数据记录 | ||
用户端 Web 自动化测试 | L3 | pageobject 设计模式 | ||
用户端 Web 自动化测试 | L3 | 异常自动截图 | ||
用户端 Web 自动化测试 | L3 | 测试用例流程设计 |
课程目标
- 掌握 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
.
├── base
│ └── base_page
├── tests
│ └── test_xxx
├── log
│ ├── test.txt
├── datas
│ └── xxx.yaml
├── page
│ ├── main_page
│ ├── xxx_page
│ └── xxx_page
└── utils
└── log_utils
代码实现
# 代码详见仓库
课堂练习
- 完成空架子搭建
- 运行用例成功即可
实战:填充测试框架
代码实现
# 代码详见仓库
实战:优化测试框架
基类中封装常用方法
将常用的方法封装到基类中,可以减少重复代码量。
Python实现
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 wait_until(self, method, timeout=10):
'''
显示等待
:param method: 等待条件
:param timeout: 超时时间
:return:
'''
ele = WebDriverWait(self.driver, timeout).until(method)
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()
Java实现
package com.hogwarts.pages;
import io.qameta.allure.Allure;
import io.qameta.allure.Step;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.List;
public abstract class BasePage {
public WebDriver driver;
public static WebDriverWait wait;
public BasePage() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--remote-allow-origins=*");
driver = new ChromeDriver(options);
//窗口最大化
driver.manage().window().maximize();
//显示等待声明
wait = new WebDriverWait(driver, Duration.ofSeconds(15), Duration.ofSeconds(2));
//todo: 隐式等待
loadPage();
}
public BasePage(WebDriver driver){
this.driver = driver;
loadPage();
}
/**
* 等待页面加载
*/
public abstract void loadPage();
@Step("元素查找:{by}")
public WebElement find(By by){
WebElement element = driver.findElement(by);
//截图
screen();
return element;
}
public void quit(){
driver.quit();//浏览器退出操作
}
//click
public void click(By by){
find(by).click();
}
//send --- clear、sendKeys
public void send(By by, String text){
WebElement element = find(by);
element.clear();
element.sendKeys(text);
}
//getText -- element.getText()
public String getText(By by){
WebElement element = find(by);
return element.getText();
}
//finds -- List<Elements>
@Step("元素查找:{by}")
public List<WebElement> finds(By by){
List<WebElement> elements = driver.findElements(by);
//截图
screen();
return elements;
}
//截图
private void screen(){
long now = System.currentTimeMillis();
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Path jpg = Paths.get("jpg", now + ".jpg");
File screenJpg = jpg.toFile();
try {
FileUtils.copyFile(screenshotAs, screenJpg);
//allure报告添加截图 -- allure添加附件
Allure.addAttachment(jpg.toString(),
"image/jpg",
Files.newInputStream(jpg),
"jpg");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
定义好之后,其他 page 中调用基类中封装好的方法即可。
添加日志
日志记录可以帮助我们更好定位和复现问题
Python实现
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)
class BasePage:
def find_ele(self, by, value):
'''
查找单个元素
:param by: 元素定位方式
:param value: 元素定位表达式
:return: 找到的元素对象
'''
logger.info(f"定位单个元素,定位方式为 {by}, 定位表达式为 {value}")
Java实现
添加日志配置
src/main/resources/logback.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
// 通过注解的方式添加日志
@Slf4j
public abstract class BasePage {
public WebElement find(By by){
log.info("元素查找:{}",by);
}
}
报错截图并保存 PageSource
在基类中添加截图和保存 page_source 的方法,当出现ui异常的时候PageSource可以帮助我们更好的定位问题。
python实现
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
java实现
//截图
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
//获取页面源码
driver.getPageSource();
课堂练习
- 完成日志添加
- 完成定位不到元素场景下截图与源码保存
实战:生成测试报告
添加 allure 步骤描述
在基类中封装的底层方法中添加 allure 步骤描述。
python实现
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
java实现
@Step("元素查找:{by}")
public WebElement find(By by){
return null;
}
allure 报告中添加文件
base_page.py 文件中,在截图和保存page_source 方法中添加文件到 allure。
python实现
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
java实现
long now = System.currentTimeMillis();
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Path jpg = Paths.get("jpg", now + ".jpg");
File screenJpg = jpg.toFile();
try {
FileUtils.copyFile(screenshotAs, screenJpg);
//allure报告添加截图 -- allure添加附件
Allure.addAttachment(jpg.toString(),
"image/jpg",
Files.newInputStream(jpg),
"jpg");
} catch (IOException e) {
throw new RuntimeException(e);
}
测试用例中添加 allure 描述
python实现
@allure.feature("企业微信 Web 端")
class TestAddMember:
@allure.story("添加成员")
@allure.title("添加成员-冒烟用例")
def test_add_member(self):
'''
添加成员冒烟用例
:return:
'''
java实现
@Step("元素查找:{by}")
public WebElement find(By by){
}
执行命令生成报告
通过命令行生成和读取报告
python实现
pytest -v --alluredir=./results --clean-alluredir
allure serve ./results
allure generate --clean alluredir results -o results/html
java实现
allure serve target/allure-results
课后作业
- 通过po完成成员添加失败用例
- 优化框架:日志打印、页面源码保存、测试报告描述
总结
- PO 模式封装 Web 自动化测试框架
- 框架优化
- 报错保存日志
- 报错截图并保存 PageSource
- 添加测试报告描述