端到端测试
POM是一种被Selenium发扬光大的端到端测试模式.
一个使用POM的项目会在测试时为页面创建的对象, 对象被用来封装关于页面的常量和方法.
本质是在测试中运用封装和重构思想.
本质是在测试中运用封装和重构思想.
例如, 对象的成员可以是以页面上的元素命名的字符串, 字符串值是元素的选择器.
通过为选择器字符串命名, 而不是直接在测试中插入字符串的方式, 可以降低维护成本.
更进一步的做法是将与选择器相关的行为也封装成方法, 在测试用例里不直接访问页面, 只使用页面对象.
通过为选择器字符串命名, 而不是直接在测试中插入字符串的方式, 可以降低维护成本.
更进一步的做法是将与选择器相关的行为也封装成方法, 在测试用例里不直接访问页面, 只使用页面对象.
在实践POM时, 人们可能对POM是否应该包含断言产生分歧:
- 包含断言显然会为POM增加过多的职责
- 不包含断言显然会导致大量样板代码被重复编写
WebDriver协议是一个已经被标准化的W3C工作草案, 源于Selenium WebDriver框架.
它定义了一种基于HTTP的协议, 实现此协议的浏览器可以提供常见的自动化功能.
它定义了一种基于HTTP的协议, 实现此协议的浏览器可以提供常见的自动化功能.
WebDriver协议在浏览器里的实现情况:
https://wpt.fyi/results/webdriver/tests?label=master&label=experimental&aligned
https://wpt.fyi/results/webdriver/tests?label=master&label=experimental&aligned
很有年头的浏览器自动化库, 它的优点是支持多种编程语言和市面上所有主流浏览器.
使用WebDriver协议(过去使用名为JSONWireProtocol的协议), 支持浏览器需要安装对应的驱动程序.
使用WebDriver协议(过去使用名为JSONWireProtocol的协议), 支持浏览器需要安装对应的驱动程序.
尽管Selenium WebDriver被用于E2E测试很长时间, 但它普遍被认为是不稳定的.
E2E领域的很多解决方案之所以成立并受到欢迎, 纯粹是因为Selenium这个比较对象的体验不好.
E2E领域的很多解决方案之所以成立并受到欢迎, 纯粹是因为Selenium这个比较对象的体验不好.
2018年宣布的Selenium 4支持调用CDP API, 然而直到2021年9月, 才刚发布第一个RC版本.
自动化测试框架, 同时支持WebDriver协议和Chrome DevTools协议, 只支持JavaScript.
本质上是Selenium WebDriver的人性化版本, 并且支持CDP比Selenium早.
测试框架为Mocha.
具有组件测试支持, 并且API比Cypress的组件测试要正常多了.
这使其能够成为已弃用的测试运行器karma的替代品.
具有组件测试支持, 并且API比Cypress的组件测试要正常多了.
这使其能够成为已弃用的测试运行器karma的替代品.
它还是Vitest的浏览器模式的适配对象之一, 建议直接通过Vitest使用它.
链式风格的测试框架.
正处于Alpha阶段.
基于WebDriver协议的iOS, Android, Windows应用测试框架.
该框架被普遍认为是缓慢, 脆弱, 复杂的.
Chrome DevTools Protocol是Chromium的调试协议.
它的性能比WebDriver协议快20%, 并且支持访问更低级的功能.
它的性能比WebDriver协议快20%, 并且支持访问更低级的功能.
由Chrome开发团队支持的专用于Chromium浏览器的自动化库,
使用DevTools协议, 用于以Headless形式使用Chromium, 它只支持JavaScript.
使用DevTools协议, 用于以Headless形式使用Chromium, 它只支持JavaScript.
Puppeteer会自动安装对应版本的Chromium浏览器,
这使得Puppeteer从来不会遇到因版本不匹配而导致的兼容性问题.
这使得Puppeteer从来不会遇到因版本不匹配而导致的兼容性问题.
该项目的主要缺陷是API有很多遗留问题.
import puppeteer from 'puppeteer-core'import path from 'path'import which from 'which'const browser = await puppeteer.launch({ executablePath: await which('chrome'), headless: false, userDataDir: path.join(process.env.LOCALAPPDATA, 'Google/Chrome/User Data')})由于Chrome的用户数据目录不能被多个Chrome实例同时读写, 以相同的userDataDir参数启动Chrome会报错.
参考:
- https://github.com/puppeteer/puppeteer/issues/4394
- https://stackoverflow.com/questions/67036041/how-to-use-user-data-dir-but-for-multiple-puppeteer-windows
- https://stackoverflow.com/questions/66583593/how-to-launch-puppeteer-with-an-already-existing-profile-multiple-time
破坏正常使用浏览器的体验.
为Chrome快捷方式添加参数
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222import puppeteer from 'puppeteer-core'const browser = await puppeteer.connect({ browserURL: 'http://127.0.0.1:9222', defaultViewport: null})- sendCharacters不模拟按键输入过程, 即不会生成对应的keydown, keyup事件.
type会模拟按键的输入过程, 即会生成对应的keydown, keyup事件. - sendCharacters可用于输入所有字符.
type不能用于输入不存在的物理键, 例如CJK字符. - sendCharacters的输入总是能成功.
type由于其模拟性质, 在一些情况下(比如页面占用的CPU时间较多时), 输入可能被吞掉.
每个浏览器上下文都有其独立的存储空间(保存cookies, localStorage等数据).
Chrome里默认浏览器上下文以外的上下文都是隐身的(incognito), 数据在上下文被关闭后就会丢失.
Chrome里默认浏览器上下文以外的上下文都是隐身的(incognito), 数据在上下文被关闭后就会丢失.
在CDP里, 可调试的对象被称作target.
Page, Frame, Worker都属于target.
Page, Frame, Worker都属于target.
target在Puppeteer里主要用于
通常是为了发现已Puppeteer API以外的方式创建的新标签页.
browser.waitForTarget 方法和target相关事件(例如 targetcreated 事件),通常是为了发现已Puppeteer API以外的方式创建的新标签页.
从某个版本起,
CDPSession对象没有引用相等性, 无法直接用来比较, 但会话的id会重用, 因此可以通过其id属性比较等价性.
page.target 方法遭到废弃, 官方推荐通过 page.createCDPSession 方法进行替代.CDPSession对象没有引用相等性, 无法直接用来比较, 但会话的id会重用, 因此可以通过其id属性比较等价性.
Chrome开发者工具中的无障碍功能树相当于该快照的实时版本.
微软开发的可用于Firefox和Chromium浏览器的自动化库, 支持多种编程语言, 使用DevTools协议.
Playwright后于Puppeteer发布, API和Puppeteer高度相似, 相当于Puppeteer的通用版本.
Playwright后于Puppeteer发布, API和Puppeteer高度相似, 相当于Puppeteer的通用版本.
Playwright还对Android和Electron平台提供实验性支持.
Playwright对非Chromium系浏览器的DevTools支持是通过补丁实现的,
这也是为什么Playwright需要下载专门的(打过补丁的)浏览器二进制文件:
https://github.com/microsoft/playwright/tree/master/browser_patches
需要注意的是, 自Firefox 86开始, Firefox实现了DevTools协议的子集.
这也是为什么Playwright需要下载专门的(打过补丁的)浏览器二进制文件:
https://github.com/microsoft/playwright/tree/master/browser_patches
需要注意的是, 自Firefox 86开始, Firefox实现了DevTools协议的子集.
已超越Cypress成为最受欢迎的E2E测试解决方案.
Playwright在很大程度上是以E2E测试为目的创建的, 与E2E相关的功能都凝聚在
这包括但不限于:
@playwright/test 里,这包括但不限于:
- 配置文件.
- 测试运行器.
- 断言库.
- 报告器.
E2E测试本身独有的强隔离性使得Playwright Test可以在很大程度上从供应商锁定的指责中逃脱.
和Cypress一样, 一旦测试涉及到需要进行复杂的setup, teardown, 以及服务器内部的状态获取,
这套测试框架就不好使了, 远不如直接用playwright库来得方便.
和Cypress一样, 一旦测试涉及到需要进行复杂的setup, teardown, 以及服务器内部的状态获取,
这套测试框架就不好使了, 远不如直接用playwright库来得方便.
链式风格的E2E测试框架.
Web E2E测试领域的明星项目, 无论是Star数还是下载量还是都远超包括Selenium在内的所有对手.
不使用Selenium, 而是实现了自己的架构, 使用已安装的Chromium系浏览器和Firefox.
Cypress的链式风格代码实际上是一种DSL, 它不仅语法奇怪, 还会造成很多不必要的问题.
无头浏览器及相关生态环境的成熟使得Cypress的竞争力进一步下降.
无头浏览器及相关生态环境的成熟使得Cypress的竞争力进一步下降.
Cypress准备测试环境时依赖于:
cy.exec调用本地程序, 通常用来调用JavaScript脚本.cy.request调用服务器API.cy.task调用插件命令(将准备测试环境的代码编写成task插件).
几乎所有与变量相关的操作都需要用Cypress的API来完成, 而不能用JavaScript来完成.
Cypress的测试框架固定为Mocha+Chai.
- 不支持多标签页, 会在新标签页打开的链接很常见, 因此会经常遇到此问题.
有两种主流的替代方案: - 1.只检测链接的URL是否正确.
- 2.删除链接的target属性后再点击.
- 不支持文件上传
- 不支持iframe(尽管有办法做到这一点)
- 不支持等待网络空闲:
https://github.com/cypress-io/cypress/issues/1773
Cypress的引导脚本可能在React 18项目上出现水合问题:
解决这个问题的方法之一是修改程序本身来注入引导脚本, 着实令人难以评价.
Cypress的代码是自动等待的, 并且会自动处理重试和超时情况, 因此不再需要像传统E2E方案那样等待状态到位.
这同时也允许Cypress的代码维持同步风格.
这同时也允许Cypress的代码维持同步风格.
Cypress里的一些API会自动具有断言功能, 这有助于在正确的位置抛出错误.
Cypress最明显的优势是具有一个调试GUI, 它提供以下重要功能:
- 详细解释测试过程
- 提供快照和时间旅行功能
- 生成页面元素的唯一选择器
一项实验性功能, 可以录制用户操作, 生成测试脚本.
Cypress提供了组件测试功能.
组件测试与端到端测试一样, 在真实浏览器里运行.
组件测试与端到端测试一样, 在真实浏览器里运行.
由于Cypress的代码风格并不真正适合组件测试场景, 并不推荐使用该功能.
虽然Cypress官方只支持功能测试, 但Cypress已经有了一些视觉测试的方案.
https://github.com/palmerhq/cypress-image-snapshot
由于Cypress不支持等待网络空闲, 因此页面里的图片加载情况会导致差异.
https://github.com/meinaart/cypress-plugin-snapshots
由于在运行所有测试用例时会出现错误, 无法使用此插件:
- https://github.com/meinaart/cypress-plugin-snapshots/issues/10
- https://github.com/cypress-io/cypress/issues/3090
尽管在Cypress里可以使用POM模式, 但并不推荐这么做, 因为:
- Cypress的链式代码本身很容易阅读.
使用POM模式会降低可读性, POM作为中间层很大程度上是多余的. - Cypress可以用"自定义命令"或者将代码片段包装成函数来重用代码, 而不是创建POM.
- Cypress鼓励用常量提取字符串, 而不是使用POM的字段.
- POM作为对象, 很容易引入额外的状态, 这对测试不利.
.get()根据CSS选择器查找元素..contains()根据文本查找元素..as()为当前选择的元素创建别名.
.should()断言一个元素应该是怎样的..and()should的别名, 语义上相当于连接之前的断言, 形成逻辑与.
.click()点击元素.dblclick()双击元素.rightclick()右键点击元素.type()输入文本..blur()取消元素的焦点.focus()使元素获得焦点.clear()清除input或textarea的值..check()选中checkbox或radio..uncheck()取消选中checkbox或radio..select()选择select中的option..trigger()根据事件名触发事件..invoke()调用jQuery方法(Cypress会自动将元素包装成jQuery元素), 常用于模拟悬停(hover)事件..invoke('show')模拟悬停
.its()取得元素/对象的属性.
.intercept()以编程方式spy/mock网络访问, 数据来源可以是fixtures目录下的文件.
使用时可能要配合.wait(), 用于等待相关的网络访问完成响应.
Cypress高度推荐使用
data-cy 作为选择器, 因为其他选择器都太脆弱.- 命名问题:
标识符的命名会成为问题, 这实际上是编程领域最大的问题之一.
此外, 还要考虑命名重复时的问题. - 生产阶段的额外成本:
如果生产阶段保留test id, 则页面大小会膨胀.
如果生产阶段通过程序手段自动移除test id, 则移除test id的需求本身就可能成为一个新的麻烦制造者. - 干扰源代码可读性和可维护性:
test id在阅读代码和搜索文本时都会成为噪音. - 虚假的安全感:
通过test id能定位到元素不意味着实际的用户能定位到元素.
另外, 其他方法的脆弱性被高估了: - 受到良好设计的CSS/XPath选择器在测试时也并没有那么脆弱.
- 代码确实变化很大的情况下, 总是会有测试会被破坏的, 这时候要是旧的测试还能通过才是问题.
- 掩盖程序的可测试性问题:
给难以定位的元素加上test id是一个非常有诱惑力的捷径式解决方案,
以至于可能掩盖真正的可测试性问题, 为未来更大的测试问题留下隐患. - 成本转嫁:
在需要应用test id的场合, 往往出现测试部门将成本被转嫁给开发部门的现象.
cy.contains 根据文本查找元素, 因此它会在文本改变后测试失败.Cypress建议在需要测试失败的情况下使用
cy.contains, 在其他场合使用 data-cy.链式风格的E2E测试框架, 不使用WebDriver或其他测试软件, 使用系统上已安装的浏览器.
Testcafe和Cypress很相似, Testcafe实际上比Cypress更早推出.
尽管Github的Star数很多, 但npm包的下载量不及Star数更少的WebdriverIO.
非常糟糕的代码气味, 会让人怀疑开发团队的品味.
Google开发的Android UI灰盒测试框架, 被认为比Appium好得多.
要求应用是Native应用.
Apple于2015年推出的UI测试框架, 建立在XCTest之上.
要求应用是Native应用.
用于React Native应用的灰盒E2E测试框架, 被认为比Appium好.
要求使用macOS和Xcode.