Web 自动化测试

霍格沃兹测试开发学社

整体大纲

  1. Web 自动化测试价值与体系

  2. 环境安装与使用

  3. 自动化用例录制,
    自动化测试用例结构分析

  4. Web 浏览器控制

  5. 常见控件定位方法

  6. 强制等待与隐式等待

  7. 常见控件交互方法

  8. 测试人论坛搜索功能自动化测试

  9. 高级定位:CSS、XPath

  10. 显式等待高级使用

  1. 高级控件交互方法

  2. 网页 frame 与多窗口处理

  3. 文件上传弹框处理

  4. 自动化关键数据记录

  5. Cypress 测试框架介绍

  6. PlayWright 测试框架介绍

  7. Web 自动化测试用例流程设计

  8. 浏览器复用,Cookie 复用

  9. Page Object 设计模式

  10. Web 自动化测试进阶实战

Web 自动化测试价值与体系

手工测试与自动化测试

uml diagram

uml diagram

什么时候可以做 UI 自动化测试

  • 业务流程相对稳定
  • 需要频繁回归
  • 核心场景等

Web 自动化测试相关技术

  • 自动化工具 :Selenium:支持多语言,行业内最火最主流
  • 测试框架 :Pytest/JUnit5
  • 测试报告 :Allure

Web 自动化测试在企业中的实践 - 有赞

有赞:很多人会认为,UI 自动化维护成本高、性价比低,但是为什么在有赞的前端质量保证体系中放在了最前面呢? 前端重用户交互,单纯的接口测试、单元测试不能真实反映用户的操作路径 ,并且从以往的经验中总结得出,因为各种不可控因素导致的发布 A 功能而 B 功能无法使用,特别是核心简单场景的不可用时有出现,所以每次发布一个应用前,都会将此应用提供的核心功能执行一遍,那随着业务的不断积累,需要回归的测试场景也越来越多,导致回归的工作量巨大。为了降低人力成本,我们亟需通过自动化手段释放劳动力,所以将核心流程回归的 UI 自动化提到了最核心地位

App 自动化测试在企业中的实践 - 美团

美团 App 的页面特点 对于不同的用户,美团 App 页面的呈现方式其实多种多样,这就是所谓的“千人千面”。以美团首页的“猜你喜欢”模块为例,针对于不同的用户有单列、Tab、双列等多种不同形式。这么多不同的页面样式需求,如果要在 1 天时间内完成开发、测试、上线流程,研发团队也面临着很大的挑战。所以测试工程师就需要重度依赖自动化测试来形成快速的验收机制。

环境安装与使用

Selenium 的简介

  • 简介
    • 用于 Web 浏览器测试的工具
    • 支持的浏览器包括 IE,Firefox,Safari,Chrome,Edge 等
    • 使用简单,可使用 Java,Python 等多种语言编写用例脚本
    • 主要由三个工具构成:WebDriver、IDE、Grid

Selenium 架构图

Selenium 环境配置步骤

  1. 准备好 Python/Java 环境
  2. 准备好 Selenium 依赖
  3. driver 的下载与配置(Mac 与 Windows 不同)
  4. 在代码中 import 对应的依赖

Selenium 的安装(Python)

  • 前提:

    • 配置好 Python 环境
    • 配置好 pip 工具
  • 安装: pip install selenium

Web Driver

Driver 的 下载 与配置步骤

selenium 4.0 开始可以自动根据浏览器的版本自动下载,但是国内下载速度不稳定,可以参考本部分的手工配置。

  1. 下载浏览器对应的 driver
  2. 配置 driver 的环境变量。
  3. 重启命令行工具
  4. 验证是否配置成功。

自动化测试脚本验证

# 导入selenium 包
from selenium import webdriver

# 创建一个 Chromdriver 的实例。Chrome()会从环境变量中寻找浏览器驱动
driver = webdriver.Chrome()
# 打开网址
driver.get("https://ceshiren.com/")
# 关闭driver
driver.quit()
# 导入selenium 包
from selenium import webdriver

# 创建一个 Geckodriver 的实例。Firefox()会从环境变量中寻找浏览器驱动
driver = webdriver.Firefox()
# 打开网址
driver.get("https://ceshiren.com/")
# 关闭driver
driver.quit()

自动化用例录制

自动化测试用例结构分析

Web 浏览器控制

浏览器控制

操作 使用场景
get 打开浏览器 Web 自动化测试第一步
refresh 浏览器刷新 模拟浏览器刷新
back 浏览器退回 模拟退回步骤
forward 浏览器前进 模拟浏览器前进步骤
maximize_window 最大化浏览器 模拟浏览器最大化
minimize_window 最小化浏览器 模拟浏览器最小化

常见控件定位方法

HTML 铺垫

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>测试人论坛</title>
  </head>
  <body>
    <a href="https://ceshiren.com/" class="link">链接</a>
  </body>
</html>
  • 标签: <a>
  • 属性:href
  • 类属性: class

Selenium 定位方式

方式 描述
class name class 属性对应的值
CSS selector(重点) CSS 表达式
id(重点) id 属性对应的值
name(重点) name 属性对应的值
link text 查找其可见文本与搜索值匹配的锚元素
partial link text 查找其可见文本包含搜索值的锚元素
如果多个元素匹配,则只会选择第一个元素
tag name 标签名称
xpath(重点) XPath 表达式

Selenium 常用定位方式

#格式:
driver.find_element_by_定位方式(定位元素)
driver.find_element(By.定位方式, 定位元素)
driver.find_element(By.ID, "su")

CSS selector 定位

  • 格式: driver.find_element(By.CSS_SELECTOR, "css表达式")
  • 复制绝对定位
  • 编写 CSS selector 表达式(后面章节详细讲解)

XPath 定位

  • 格式: driver.find_element(By.XPATH, "xpath表达式")
  • 复制绝对定位
  • 编写 XPath 表达式(后面章节详细讲解)

  • 格式:
    • driver.find_element(By.LINK_TEXT,"文本信息")

强制等待与隐式等待

为什么要添加等待

  • 页面加载延迟问题
  • 动态加载问题
  • 状态变化问题
from selenium import webdriver
from selenium.webdriver.common.by import By

def test_sleep():
    """
    如果直接执行,不添加任何等待,可能会报错
    """
    driver = webdriver.Chrome()
    driver.get("https://vip.ceshiren.com/")
    # 不加等待,可能会因为网速等原因产生报错
    driver.find_element(By.XPATH, "//*[text()='个人中心']")

直接等待

  • 解决方案:在报错的元素操作之前添加等待
  • 原理:强制等待,线程休眠一定时间
  • 演练环境:https://vip.ceshiren.com/
  • time.sleep(3)
from selenium import webdriver
from selenium.webdriver.common.by import By

def test_sleep():
    """
    如果直接执行,不添加任何等待,可能会报错
    """
    driver = webdriver.Chrome()
    driver.get("https://vip.ceshiren.com/")
    # 添加等待,让页面渲染完成
    time.sleep(3)
    driver.find_element(By.XPATH, "//*[text()='个人中心']")

隐式等待

  • 问题:难以确定元素加载的具体等待时间。
  • 解决方案:针对于寻找元素的这个动作,使用隐式等待添加配置。
  • 演练环境:https://vip.ceshiren.com/
  • 原理:设置一个等待时间,轮询查找(默认 0.5 秒)元素是否出现,如果没出现就抛出异常
#设置一个等待时间,轮询查找(默认0.5秒)元素是否出现,如果没出现就抛出异常
driver.implicitly_wait(3)

隐式等待无法解决的问题

  • 案例:元素可以找到,但是后续操作出现报错
  • 原因:
    • 页面元素加载是异步加载过程,通常 HTML 会先加载完成,js、css 其后
    • 元素存在与否是由 HTML 决定,元素的交互是由 CSS 或者 JS 决定
    • 隐式等待只关注元素能不能找到,不关注元素能否点击或者进行其他的交互
  • 解决方案:使用显式等待

显式等待基本使用(初级)

  • 示例: WebDriverWait(driver实例, 最长等待时间, 轮询时间).until(结束条件)
  • 原理:在最长等待时间内,轮询,是否满足结束条件
  • 演练环境: https://vip.ceshiren.com/#/ui_study
  • 注意:在初级时期,先关注使用
def wait_until():
    driver = webdriver.Chrome()
    driver.get("https://vip.ceshiren.com/#/ui_study")
    WebDriverWait(driver, 10).until(
        expected_conditions.element_to_be_clickable(
            (By.CSS_SELECTOR, '#success_btn')))
    driver.find_element(By.CSS_SELECTOR, "#success_btn").click()

总结

类型 使用方式 原理 适用场景
直接等待 time.sleep(等待时间)) 强制线程等待 调试代码,临时性添加
隐式等待 driver.implicitly_wait(等待时间) 在时间范围内,轮询查找元素 解决找不到元素问题,无法解决交互问题
显式等待 WebDriverWait(driver实例, 最长等待时间, 轮询时间).until(结束条件) 设定特定的等待条件,轮询操作 解决特定条件下的等待问题,比如点击等交互性行为

常见控件交互方法

常见控件交互 API

  • click
  • send_keys
  • get_attribute

高级定位:CSS、XPath

CSS 相对定位的优点

  • 可维护性更强
  • 语法更加简洁
  • 解决各种复杂的定位场景
# 绝对定位
$("#ember63 > td.main-link.clearfix.topic-list-data > span > span > a")
# 相对定位
$("#ember63 [title='新话题']")

CSS 定位的调试方法

  • 进入浏览器的 console
  • 输入:
    • $("css表达式")
    • 或者 $$("css表达式")

CSS 基础语法

类型 表达式
标签 标签名
.class属性值
ID #id属性值
属性 [属性名='属性值']
//在console中的写法
// https://www.baidu.com/
//标签名
$("input");
//.类属性值
$(".s_ipt");
//#id属性值
$("#kw");
//[属性名='属性值']
$('[name="wd"]');

CSS 关系定位

类型 格式
并集 元素,元素
邻近兄弟(了解即可) 元素+元素
兄弟(了解即可) 元素 1~元素 2
父子 元素>元素
后代 元素 元素
//在console中的写法
//元素,元素
$(".bg,.s_ipt_wr,.new-pmd,.quickdelete-wrap");
//元素>元素
$("#s_kw_wrap>input");
//元素 元素
$("#form input");
//元素+元素,了解即可
$(".soutu-btn+input");
//元素1~元素2,了解即可
$(".soutu-btn~i");

CSS 顺序关系

类型 格式
父子关系+顺序 nth-child
父子关系+标签类型+顺序 nth-of-type
//:nth-child(n)
$('#form>input:nth-child(2)')
//:nth-of-type(n)
$('#form>input:nth-of-type(1)')

XPath 定位的调试方法

  • 浏览器-console
    • $x("xpath表达式")
  • 浏览器-elements
    • ctrl+f 输入 XPath 或者 CSS

XPath 基础语法(包含关系)

表达式 结果
/ 从该节点的子元素选取
// 从该节点的子孙元素选取
* 通配符
nodename 选取此节点的所有子节点
.. 选取当前节点的父节点
@ 选取属性
# 整个页面
$x("/")
# 页面中的所有的子元素
$x("/*")
# 整个页面中的所有元素
$x("//*")
# 查找页面上面所有的div标签节点
$x("//div")
# 查找id属性为site-logo的节点
$x('//*[@id="site-logo"]')
# 查找节点的父节点
$x('//*[@id="site-logo"]/..')

XPath 顺序关系(索引)

  • XPath 通过索引直接获取对应元素
# 获取此节点下的所有的li元素
$x("//*[@id='ember21']//li")
# 获取此节点下【所有的节点的】第一个li元素
$x("//*[@id='ember21']//li[1]")

XPath 高级用法

  • [last()] : 选取最后一个
  • [@属性名='属性值' and @属性名='属性值'] : 与关系
  • [@属性名='属性值' or @属性名='属性值'] : 或关系
  • [text()='文本信息'] : 根据文本信息定位
  • [contains(text(),'文本信息')] : 根据文本信息包含定位
  • 注意:所有的表达式需要和 [] 结合
# 选取最后一个input标签
//input[last()]
# 选取属性name的值为passward并且属性pwd的值为123456的input标签
//input[@name='passward' and @pwd='123456']
# 选取属性name的值为passward或属性pwd的值为123456的input标签
//input[@name='passward' or @pwd='123456']
# 选取所有文本信息为'霍格沃兹测试开发'的元素
//*[text()='霍格沃兹测试开发']
# 选取所有文本信息包'霍格沃兹'的元素
//*[contains(text(),'霍格沃兹')]

Web 自动化测试用例流程设计

Page Object 设计模式

Page Object 模式简介

传统 UI 自动化的问题

  • 无法适应 UI 频繁变化
  • 无法清晰表达业务用例场景
  • 大量的样板代码 driver/find/click

PO 模式的优势

  • 降低 UI 变化导致的测试用例脆弱性问题
  • 让用例清晰明朗,与具体实现无关

POM 建模原则

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

PO 使用方法

  • 把元素信息和操作细节封装到 PageObject 类中
  • 根据业务逻辑,在测试用例中调用

测试人论坛搜索功能自动化测试

Web 自动化测试

第二课

显式等待高级使用

高级控件交互方法

ActionChains 拖拽示例

    draggable = driver.find_element(By.ID, "draggable")
    droppable = driver.find_element(By.ID, "droppable")
    ActionChains(driver)\
        .drag_and_drop(draggable, droppable)\
        .perform()

网页 frame 与多窗口处理

Frame 处理

<div id="modal">
  <iframe id="buttonframe" name="myframe"  src="https://seleniumhq.github.io">
   <button>Click here</button>
 </iframe>
</div>
iframe = driver.find_element(By.CSS_SELECTOR, "#modal > iframe")
driver.switch_to.frame(iframe)
driver.find_element(By.TAG_NAME, 'button').click()
driver.switch_to.default_content()

多窗口处理

driver.current_window_handle
driver.window_handles
driver.switch_to.window(window_handle)
driver.switch_to.new_window('tab')
driver.switch_to.new_window('window')

文件上传弹框处理

百度图片上传示例

    def test_upload(self):
        self.driver.get('https://www.baidu.com/')
        self.driver.find_element(By.CLASS_NAME, 'soutu-btn').click()
        self.driver.find_element(By.CLASS_NAME, 'upload-pic') \
            .send_keys('/Users/seveniruby/Dropbox/sihanjishu/startup/霍格沃兹测试学院/banner/通用素材/学院长方形banner.png')

自动化关键数据记录

使用 Allure 报告框架记录日志

    def test_allure(self):
        self.driver.get('https://ceshiren.com/')
        allure.attach(body=self.driver.get_screenshot_as_png(), attachment_type= allure.attachment_type.PNG)
        self.driver.find_element(By.ID, 'search-button').click()
        allure.attach(body=self.driver.get_screenshot_as_png(), attachment_type=allure.attachment_type.PNG)

生成 allure 报告

pytest --alluredir=allure  geektime_0/web/wework/reuse.py
allure serve allure/

启动带有远程调试功能的浏览器

# windows:
chrome --remote-debugging-port=9222
#mac
Google\ Chrome --remote-debugging-port=9222

#执行后的输出
hogwarts: ~ seveniruby$ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome  --remote-debugging-port=9222
DevTools listening on ws://127.0.0.1:9222/devtools/browser/e00051cf-6a06-44be-ae6f-a8dec8c75410
from selenium.webdriver.chrome.options import Options
option = Options()
option.debugger_address = "localhost:9222"
self._driver = webdriver.Chrome(options=option)
self._driver.get("https://work.weixin.qq.com/wework_admin/frame")
  • 获取 cookie driver.get_cookies()
  • 添加 cookie driver.add_cookie(cookie)
class TestCookieLogin:
    def setup_class(self):
        self.drvier = webdriver.Chrome()

    def test_get_cookies(self):
        # 1. 访问企业微信主页/登录页面
        self.drvier.get("https://work.weixin.qq.com/wework_admin/frame#contacts")
        # 2. 等待20s,人工扫码操作 或者使用显式等待url发生变化
        time.sleep(20)
        # 3. 等成功登陆之后,再去获取cookie信息
        cookie = self.drvier.get_cookies()
        # 4. 将cookie存入一个可持久存储的地方,文件
        # 打开文件的时候添加写入权限
        with open("cookie.yaml", "w") as f:
            # 第一个参数是要写入的数据
            yaml.safe_dump(cookie, f)

    def test_add_cookie(self):
        # 1. 访问企业微信主页面
        self.drvier.get("https://work.weixin.qq.com/wework_admin/frame#contacts")
        # 2. 定义cookie,cookie信息从已经写入的cookie文件中获取
        cookie = yaml.safe_load(open("cookie.yaml"))
        # 3. 植入cookie
        for c in cookie:
            self.drvier.add_cookie(c)
        time.sleep(3)
        # 4.再次访问企业微信页面,发现无需扫码自动登录,而且可以多次使用
        self.drvier.get("https://work.weixin.qq.com/wework_admin/frame#contacts")

企业微信 Web 自动化测试实战

Cypress 测试框架介绍

cypress 简介

  • 基于 JavaScript 的前端测试工具
  • 可以对浏览器中运行的任何内容进行快速、简单、可靠的测试
  • 对每一步操作都支持回看
  • 覆盖了测试金字塔模型的所有测试类型【界面测试,集成测试,单元测试】
  • 底层协议不采用 WebDriver

cypress 与 selenium 对比

项目 Cypress Selenium
支持语言 Javascript Java, Python, Javascript, Ruby, C#等
支持浏览器 Chrome、Electron 各种主流浏览器
主要使用者 前端开发人员 QA
使用的测试框架 Mocha 无限制
是否需要浏览器驱动器 需要
测试速度 略慢
录制测试视频、快照 支持 支持,但需要写代码
社区支持 略显薄弱 强大

Cypress 示例

describe("Post Resource", () => {
  it("Creating a New Post", () => {
    cy.visit("/posts/new"); // 1.

    cy.get("input.post-title") // 2.
      .type("My First Post"); // 3.

    cy.get("input.post-body") // 4.
      .type("Hello, world!"); // 5.

    cy.contains("Submit") // 6.
      .click(); // 7.

    cy.get("h1") // 8.
      .should("contain", "My First Post");
  });
});

Playwright 测试框架介绍

PlayWright 架构

PlayWright 代码示例 python

import re
from playwright.sync_api import Page, expect


def test_homepage_has_Playwright_in_title_and_get_started_link_linking_to_the_intro_page(page: Page):
    page.goto("https://playwright.dev/")

    # Expect a title "to contain" a substring.
    expect(page).to_have_title(re.compile("Playwright"))

    # create a locator
    get_started = page.locator("text=Get Started")

    # Expect an attribute "to be strictly equal" to the value.
    expect(get_started).to_have_attribute("href", "/docs/intro")

    # Click the get started link.
    get_started.click()

    # Expects the URL to contain intro.
    expect(page).to_have_url(re.compile(".*intro"))

Q&A