Skip to content

【线上】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 自动化测试框架搭建
  • 在自动化测试框架中编写企业微信添加成员测试用例
  • 优化测试框架
  • 输出测试报告

实战思路

uml diagram

实战:使用 PO 模式封装测试框架

线性脚本中存在的问题
  • 无法适应 UI 频繁变化
  • 无法清晰表达业务用例场景
  • 大量的样板代码 driver/find/click
解决方案
  • 领域模型适配:封装业务实现,实现业务管理
  • 提高效率:降低用例维护成本,提高执行效率
  • 增强功能:解决已有框架不满足的情况
Page Object 模式简介

PO 模式(Page Object Model)是自动化测试项目开发实践的最佳设计模式之一。

它的主要用途是把一个具体的页面转换成编程语言当中的一个对象,页面特性转化成对象属性,页面操作转换成对象方法。

PO 思想最开始来源于马丁福勒(Marktin Flewer)在 2004 年发表的一篇文章。最初是叫作 Window driver,后来 selenium 沿用这种思想,后来就改成了 POM。

PO 模式的核心思想是通过对界面元素的封装减少冗余代码,同时在后期维护中,若元素定位发生变化,只需要调整页面元素封装的代码,提高测试用例的可维护性、可读性。

image

马丁福勒个人博客 selenium 官方网站推荐

PO 模式的优势
  • 降低 UI 变化导致的测试用例脆弱性问题
  • 让用例清晰明朗,与具体实现无关
PO 模式建模原则
  • 属性意义

    • 不要暴露页面内部的元素给外部。
    • 不需要建模 UI 内的所有元素。
  • 方法意义
    • 用公共方法代表 UI 所提供的功能。
    • 方法应该返回其他的 PageObject 或者返回用于断言的数据。
    • 同样的行为不同的结果可以建模为不同的方法。
    • 不要在方法内加断言。
企业微信 Web 端 PO 建模

以下为添加成员功能的原型图。

原型图

根据原型图可以梳理添加成员的业务逻辑如下:

  • 方块代表一个类
  • 每条线代表这个页面提供的方法
  • 箭头的始端为开始页面
  • 箭头的末端为跳转页面或需要断言的数据

uml diagram

页面元素和服务梳理

清楚业务逻辑后,即可梳理出每个页面包含和元素和需要提供的服务。

uml diagram

项目结构

项目的整个结构如下图所示。

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)
其他页面导入 logger 即可定义不同级别的日志。

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>
其他页面导入 logger 即可定义不同级别的日志。

// 通过注解的方式添加日志
@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
课后作业
  1. 通过po完成成员添加失败用例
  2. 优化框架:日志打印、页面源码保存、测试报告描述

总结

  • PO 模式封装 Web 自动化测试框架
  • 框架优化
    • 报错保存日志
    • 报错截图并保存 PageSource
    • 添加测试报告描述