- 测试
【免费下载链接】protractor
E2E test framework for Angular apps
本文基于本仓库 docs/system-setup.md 展开,系统讲解 Protractor 端到端测试中被测系统的选择与准备方式:为什么只需一个 URL 即可测试任意环境(本地开发、内网预发布乃至公网生产)、手动 bootstrap 与轮询型页面两类特殊场景的规避方案,以及通过
onPrepare完成登录等全局预置的正确姿势。读完本文,你将能够为任意 Protractor 项目正确配置被测系统,并写出同步可靠、无竞态条件的测试启动流程。
一、被测系统的本质:真实浏览器 + 一个 URL
Protractor 与许多基于 mock 的单元测试框架最大的不同在于:它使用真实浏览器执行测试。浏览器能访问什么,Protractor 就能测试什么,因此你在决定"测什么"这件事上拥有极大的自由度:
- 一个跑在
localhost上的本地开发服务器; - 内网中的预发布(staging)服务器;
- 甚至公网上直接面向用户的正式(production)服务器。
对 Protractor 而言,它唯一真正需要的东西就是URL。测试启动时通过browser.get(url)加载被测页面,后续所有查找元素、断言、点击等操作都发生在真实浏览器渲染出的页面中。这一点也让 Protractor 与 WebDriver 生态天然对齐:底层连接的可以是本地 Selenium Server、远程 Selenium 服务或直连浏览器驱动,具体连接方式由配置文件中的seleniumAddress、seleniumServerJar、directConnect等选项决定(详见 lib/config.ts)。
不过在"只要给 URL 就行"的宽松背后,文档明确警告了三类必须小心的情况,下面逐一展开。
二、注意事项一:手动 bootstrap 的页面 —— 请使用browser.driver.get
正常情况下,browser.get会在页面加载后主动等待 Angular 完成引导(bootstrap)并进入稳定状态。但如果你的页面不是由 Angular 自动引导,而是采用手动 bootstrap(例如在代码中显式调用angular.bootstrap()或使用ngUpgrade之外的定制引导流程),Protractor 将无法识别并加载该页面。
文档给出的解决方案是:放弃browser.get,改用基础 WebDriver 实例browser.driver.get:
// 手动 bootstrap 页面无法用 browser.get 加载 // browser.get('/ng1/login.html'); // ❌ 不可靠 // 改用基础 webdriver 实例 await browser.driver.get('http://localhost:8080/login.html');browser.driver是 Protractor 对外暴露的原生WebDriver实例(在 lib/browser.ts 中构造),它完全绕过 Angular 同步机制,直接把命令交给浏览器执行。这带来一个连锁后果:
Protractor 不再知道页面何时加载完成,你需要自行添加
wait语句,否则测试容易产生竞态条件(race condition)——元素尚未渲染就开始点击/断言,导致偶发失败。
这正是仓库登录测试配置 spec/withLoginConf.js 中的做法:登录页是非 Angular 手动引导页面,因此先用browser.driver.get打开,再配合browser.driver.wait显式等待登录跳转完成(详见下文第四节)。
何时用browser.driver.get的经验法则:
- 页面完全不依赖 Angular,或 Angular 引导由自定义逻辑触发;
- 页面是纯 WebDriver 流程的一部分(如测试前的登录页);
- 只要使用了
browser.driver.get,就应当配套显式等待逻辑,不能依赖 Protractor 的自动同步。
三、注意事项二:用$timeout轮询的页面 —— 改用$interval
Protractor 的核心同步机制是:执行任何操作前,它会等待 Angular 应用没有待处理(pending)的异步任务。如果你在 AngularJS 应用中使用$timeout做持续轮询,那么同一时刻永远有未完成的异步任务,Protractor 会一直等待下去,直到触发超时。
文档明确给出建议:把轮询用的$timeout换成$interval。
// ❌ 持续轮询 + $timeout:Protractor 永远等不到稳定 // $timeout(poll, 1000); // ✅ 持续轮询请使用 $interval(AngularJS 1.2rc3 起提供) $interval(poll, 1000);这一论断在 docs/timeouts.md 中有更详细的佐证:如果 AngularJS 应用持续轮询$timeout或$http,Protractor 会无限等待并超时;轮询场景应使用$interval。对于使用 Zone.js 的 Angular(2+)应用,类似的思路是:把长期运行的异步任务放到 Angular Zone 之外执行,避免阻塞测试继续。
从底层看,Protractor 通过注入脚本装饰$timeout/$interval来追踪待处理任务(配置项untrackOutstandingTimeouts与此相关,见 lib/config.ts)。因此,被测应用能否被正确同步,本质上取决于应用侧异步 API 的选择——这也是"设置被测系统"环节需要重点审查的代码层面因素。
四、注意事项三:全局测试准备 ——onPrepare的三种形态
如果测试前需要做全局准备(最典型的场景是登录),文档要求你把它放进配置文件的onPrepare属性中。onPrepare是一个多态(polymorphic)属性,既可以是函数,也可以是文件名:
- 函数形态:直接内联在配置文件里,例如 spec/onPrepareConf.js:
exports.config = { framework: 'jasmine', specs: ['onPrepare/*_spec.js'], baseUrl: env.baseUrl + '/ng1/', onPrepare: () => { browser.params.password = '12345'; // 预设一个全局参数 } };- 文件形态:传入相对路径字符串,Protractor 会用 Node.js 的
require加载该文件并执行其内容。例如 spec/onPrepareFileConf.js 与对应的 spec/onPrepare/startup.js:
// onPrepareFileConf.js exports.config = { specs: ['onPrepare/*_spec.js'], baseUrl: env.baseUrl + '/ng1/', onPrepare: 'onPrepare/startup.js' // 字符串 → 文件路径 };// onPrepare/startup.js —— 文件内容即为要执行的准备逻辑 browser.params.password = '12345';文件形态下,路径是相对于配置文件所在目录解析的——这一点在 lib/configParser.ts 中有明确实现:onPrepare与其他文件路径类配置(如seleniumServerJar、chromeDriver、firefoxPath)一起,被统一转换为相对于当前配置文件的路径。
验证这两种形态等价性的测试见 spec/onPrepare/onPrepare_spec.js,其中的断言expect(browser.params.password).toEqual('12345')证明onPrepare中写入的browser.params确实在测试用例中生效。
4.1onPrepare可以返回 Promise —— 时序保证的关键
文档强调了一个容易被忽略但至关重要的细节:
onPrepare可以返回一个 Promise,Protractor 会等待它完成后再继续执行。当准备过程涉及异步调用(例如与浏览器交互)时,必须这样做;否则 Protractor 无法保证执行顺序,可能在准备完成前就开始跑测试。
也就是说,如果onPrepare里有异步操作,却不返回 Promise,测试与准备之间就会出现竞态。正确写法是让onPrepare变成async函数或显式返回 Promise,例如 spec/onPreparePromiseConf.js:
onPrepare: async() => { browser.params.password = '12345'; return await new Promise(resolve => { setTimeout(resolve, 1000); // 模拟耗时准备 }); }文件形态同样支持返回 Promise——文件导出一个async函数即可,见 spec/onPreparePromiseFileConf.js 对应的 spec/onPrepare/asyncstartup.js:
module.exports = async() => { browser.params.password = '12345'; return await new Promise((resolve) => { setTimeout(resolve, 1000); }); }4.2 源码级验证:onPrepare到底怎么被执行的
onPrepare的"函数或文件名二选一"语义,由 lib/util.ts 中的内部助手runFilenameOrFn_统一实现:
- 若传入的是字符串,则以
configDir为基准执行require(path.resolve(configDir, filenameOrFn))加载文件; - 加载结果若为函数,则
await filenameOrFn.apply(null, args)调用它; await保证了返回的 Promise 会被等待——这正是"Protractor 会等 onPrepare 的 Promise 完成"这一行为的技术根基。
调用链上,lib/runner.ts 在setTestPreparer(config.onPrepare)(约 L61)注册准备函数,随后在真正执行用例前先运行插件准备、再执行runFilenameOrFn_(this.config_.configDir, this.preparer_)(约 L92-L94)。结合 lib/config.ts 对onPrepare的注释还可以确认:当使用 multiCapabilities 并行跑多个浏览器时,onPrepare会对每个 capability 各执行一次;且此时 Protractor 全局对象与测试框架全局(如 Jasmine)都可用,你可以利用browser.getProcessedConfig()读取当前正在执行的 capability。
五、实战案例:用onPrepare完成登录(完整可复制)
仓库在 spec/withLoginConf.js 中提供了一个完整的"测试前登录"配置,是系统设置文档直接推荐的范例。完整解读如下:
const env = require('./environment.js'); exports.config = { seleniumAddress: env.seleniumAddress, // 连接已运行的 Selenium Server SELENIUM_PROMISE_MANAGER: false, // 关闭 WebDriver 控制流,改用 async/await framework: 'jasmine', specs: ['login/login_spec.js'], // 待运行的测试 capabilities: env.capabilities, // 浏览器能力 baseUrl: env.baseUrl + '/ng1/', // 被测应用基地址 onPrepare: async() => { // 1) 登录页是非 Angular 手动 bootstrap 页面,必须用 browser.driver.get await browser.driver.get(env.baseUrl + '/ng1/login.html'); // 2) 直接通过 WebDriver 原生 API 填写表单 await browser.driver.findElement(by.id('username')).sendKeys('Jane'); await browser.driver.findElement(by.id('password')).sendKeys('1234'); await browser.driver.findElement(by.id('clickme')).click(); // 3) 登录需要时间:显式等待跳转到 index.html 才继续 return await browser.driver.wait(async() => { const url = await browser.driver.getCurrentUrl(); return /index/.test(url); }, 10000); } };这个例子把本文第二、四节的要点全部落地:
- 登录页不可用
browser.get→ 用browser.driver.get打开,并全程使用browser.driver.findElement(而非element(...))操作表单; - 准备是异步的→
onPrepare声明为async并返回 Promise(return await browser.driver.wait(...)); - 显式等待消除竞态→ 用
browser.driver.wait轮询当前 URL 是否包含index,超时上限 10 秒,确认登录跳转完成后才放行测试。
配套的测试用例见 spec/login/login_spec.js:登录态通过 cookie 保持,测试直接browser.get(baseUrl + '/ng1/index.html')进入主页面,断言登录后的 cookietestcookie值为Jane-1234,从而验证onPrepare建立的会话确实在整个测试过程中有效。
六、为被测系统补齐配套配置
除了onPrepare与baseUrl,搭建被测系统时通常还会涉及以下配置(均出自 lib/config.ts 的完整注释):
| 配置项 | 作用 | 说明 |
|---|---|---|
baseUrl | 被测应用基地址 | browser.get()传入相对路径时会基于它解析(url.resolve语义),见 lib/config.ts |
getPageTimeout | 页面加载超时(毫秒) | 默认 10 秒,超时错误形如Timed out waiting for page to load after 10000ms,可全局配置或按次传参browser.get(address, timeout),见 docs/timeouts.md |
allScriptsTimeout | Angular 同步超时(毫秒) | 默认约 11 秒,若应用异步任务迟迟不结束会触发,见 docs/timeouts.md |
capabilities/multiCapabilities | 单/多浏览器能力 | 可附加count、shardTestFiles、maxInstances等并行参数,见 lib/config.ts |
specs/suites | 指定要跑的测试 | specs为相对当前配置文件的路径模式,见 lib/config.ts |
两个直接的工程建议:
- 为被测应用准备多种环境地址:由于被测系统只需 URL,可在配置中按环境切换
baseUrl(如http://localhost:4200与https://staging.example.com),配合params甚至命令行参数(--params.xxx,见 lib/config.ts)实现一套配置多环境复用; - 把"页面是否由 Angular 引导、是否持续轮询"当作被测系统的验收标准:上线测试前先检查应用侧这两类代码特征,能从根本上避免同步超时与竞态,远比在测试侧打补丁可靠。
小结
"设置被测系统"对 Protractor 来说,本质是回答三个问题:被测页面能不能被browser.get正常加载?应用会不会让 Protractor 永远等不到稳定?进入主流程前需要做什么全局准备?本文从 docs/system-setup.md 出发,结合仓库源码与配置示例给出了完整答案:手动 bootstrap 页面用browser.driver.get+ 显式等待;轮询用$interval替代$timeout;登录等全局准备放进onPrepare(函数或文件皆可,异步时必须返回 Promise)。沿着这三条主线,即可在任意环境上搭建出稳定、可复现的端到端测试被测系统。
- 测试
【免费下载链接】protractor
E2E test framework for Angular apps
相关推荐
微信QQ防撤回补丁教程:4步装好补丁,让对方撤回无效
微信QQ防撤回补丁教程:4步装好补丁,让对方撤回无效 晚上十一点四十,客户在群里发来"发票金额没错,按这个走",几秒后这条消息变成一行灰字。第二天早上你追问时,
桌面应用即时通讯STF 测试指南:基于 Karma 与 Protractor 的前端单元测试和端到端测试实战
STF 测试指南:基于 Karma 与 Protractor 的前端单元测试和端到端测试实战 本文以 Smartphone Test Farm(STF)仓库的
测试后端前端json.lua 错误处理完全指南:如何精准定位 JSON 解析问题
json.lua 错误处理完全指南:如何精准定位 JSON 解析问题 JSON 解析是应用开发中常见的任务,但错误处理往往被忽视。本文将深入探讨 Lua 轻量级
序列化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考