Skip to content

【周末】web自动化测试实战

Web 自动化测试实战

直播前准备

  • 重点学习 Page Object 设计模式
专题课 阶段 章节 Python 班级
用户端 Web 自动化测试 L2 显式等待高级使用 Python 版录播
用户端 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
.
├── __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
    • 添加测试报告描述