Skip to content

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

实战思路

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
    • 添加测试报告描述