大家都知道在不同浏览器上测试代码有多重要。多数时候我会觉得,开发者社区中的朋友们这一点做得非常棒 —— 至少是在初次发布项目的时候。
测试做得不好的是在每次修改代码的时候。
我个人也为此内疚。“自动化、跨浏览器的 JavaScript 单元测试”,这在我的 todo list 中已陈列数年,可每次坐下来打算认真弄明白时,又放弃了。我知道,有一部分是惰性所致,不过此话题的优质信息的惊人匮乏也难辞其咎。
有很多工具和框架(如 Karma)声称“使自动化,JavaScript 测试变简单”,但经验告诉我,这些工具所引入的复杂度,远超出它们所摆脱的麻烦(待会儿再细说)。在我的经验中,只要“工作就行”的工具,对专家来说是很好,不过学起来麻烦。我想要的是,理解这个过程在底层如何工作,这样万一测试程序挂掉(该来的最后总会来),我也能修复它。
对我来说,理解工作原理的最佳方法就是从头开始重造一遍。因此,我决定自己来造轮子,与社区分享我所学到的东西。
我写这篇文章,因为这正是几年前我刚开始发布开源项目时希望找到的。如果你自己从来没试过自动化、跨浏览器的 JavaScript 单元测试,但一直想学,那本文就是为你而写的。本文会向你解释整个工作过程,展示如何动手。
手动测试
在介绍自动化测试之前,我觉得有必要了解同样的页面在手动测试中是怎样的。
毕竟,自动化就是使用机器以解决现有工作流中的重复部分。没有完整了解手动测试,就想开始玩自动化,也不太可能理解自动化流程。
在手动测试中,在测试文件中编写测试用例,可能像下面这样:
var assert = require('assert');
var SomeClass = require('../lib/some-class');
describe('SomeClass', function() {
describe('someMethod', function() {
it('accepts thing A and transforms it into thing B', function() {
var sc = new SomeClass();
assert.equal(sc.someMethod('A'), 'B');
});
});
});
上面这个例子使用 Mocha 和 Node.js 的 assert
模块,不过使用哪种测试或断言库并不是那么重要,用什么都行。
Mocha 在 Node.js 中运行,你可能会在命令行中运行下面的命令:
mocha test/some-class-test.js`
要在浏览器中运行测试,需要一个 HTML 文件,文件中用 script 标签来加载脚本;又因为浏览器不理解 require
声明,你还需要一个像 browserify 或 webpack 这样的模块打包工具来解析依赖。
browserify test/*-test.js > test/index.js
很棒的是,这些模块打包工具会将所有测试文件(以及其他任何依赖)打包为一个文件,这样在测试页面中加载就变得简单了。[1]
通常使用 Mocha 的测试文件长这样:
Tests
修改成:
以上代码与 Mocha 样板默认代码的唯一区别在于,该逻辑将测试结果赋值给 Sauce Labs 所期望的 window.mochaResults
变量。新的代码不会影响浏览器中的手动测试,所以还可以像原来一样使用。
再强调此前提过的一点,Sauce Labs “运行”测试的时候,并非真正在运行什么东西,它只是访问一个网页,然后等到 window.mochaResults
对象被发现有值了。然后再记录结果。
判断测试是否通过
开始 JS 单元测试 (Start JS Unit Tests) 方法告诉 Sauce Labs 按照队列在你所要求的浏览器/平台中运行测试,但它并未范围测试结果。
它所返回的仅仅只是队列中任务的 ID。响应大概像下面这样:
{
"js tests": [
"9b6a2d7e6c8d4fd2afeeb0ff7e54e694",
"d38688ec7256497da6966f4523ddee76",
"14054e68ccd344c0bed77a798a9ce1e8",
"dbc54181f7d947458f52201ea5fcb901"
]
}
要判断测试是否通过,可以调用获取 JS 单元测试状态(Get JS Unit Test Status)方法,这个方面接受一个任务 ID 列表,返回每个任务的当前状态。
定期调用这个方法,直到所有的工作完成。
request({
url: `https://saucelabs.com/rest/v1/${username}/js-tests/status`,
method: 'POST',
auth: {
username: process.env.SAUCE_USERNAME,
password: process.env.SAUCE_ACCESS_KEY
},
json: true,
body: jsTests, // The response.body from the first API call.
}, (err, response) => {
if (err) {
console.error(err);
} else {
console.log(response.body);
}
});
响应大致如下:
{
"completed": false,
"js tests": [
{
"url": "https://saucelabs.com/jobs/75ac4cadb85e415fae957f7811d778b8",
"platform": [
"Windows 10",
"chrome",
"latest"
],
"result": {
"passes": 29,
"tests": 30,
"end": {},
"suites": 7,
"reports": [],
"start": {},
"duration": 97,
"failures": 0,
"pending": 1
},
"id": "1f74a237d5ba4a47b5a42570ae1e7999",
"job_id": "75ac4cadb85e415fae957f7811d778b8"
},
// ... the rest of the jobs
]
}
一旦 response.body.complete
属性变为 true
,所有测试结束,然后就能遍历以获取测试结果。
通过 localhost 测试
前面说过,Sauce Labs 是通过访问 URL “运行”测试的。所以这也意味着你所使用的 URL 必须是互联网上能公开访问的。
这样一来,如果要通过 localhost
跑测试,那就有问题了。
已经有一些解决该问题的工具了,包括 Sauce Connect (官方推荐),这是 Sauce Labs 创建的一个代理服务器,它在本地服务器与 Sauce Labs 虚拟机之间开了一个安全连接。
Sauce Connect 是按照安全的要求来设计的,通过它,外人几乎不可能访问到你的代码。不过它的一个缺点是配置、使用极其复杂。
如果代码安全性很重要,Sauce Connect 可能值得一试;否则的话,还有一些更简单的类似方案。
我选择的是 ngrok。
ngrok
ngrok 是建立与 localhost 之间的安全信道(secure tunnels)的工具。它提供一个公开的 URL[2] 给本地的 Web 服务器,这个 URL 恰恰就是在 Sauce Labs 上运行测试所需要的。
如果你在虚拟机上做过开发或手动测试,可能已经听说过 ngrok;如果没有,建议你好好去看看。这是个非常有用的工具。
在开发机上安装 ngrok,和下载二进制文件、加入到 PATH 一样简单;不过,如果你要在 Node 中使用 ngrok,最后还是通过 npm 来安装。
npm install ngrok
在 Node 中通过以下代码,可以开启一个 ngrok 进程:
const ngrok = require('ngrok');
ngrok.connect(port, (err, url) => {
if (err) {
console.error(err);
} else {
console.log(`Tests now accessible at: ${url}`);
}
});
一旦有了测试文件的公开 URL,使用 Sauce Labs 跨浏览器测试本地代码基本上就很简单啦!
过程整合
本文涵盖了不少话题,可能会给人以这样的印象:自动化跨浏览器的 JavaScript 单元测试很复杂。但实际上并不是这样的。
文章的框架是从我的角度来设定的 —— 我在试着解决自己的问题。而且就我自己的经验来看,真正的复杂之处仅仅是因为缺乏相关信息,整个过程如何工作,如何将所有这些整合起来。
如果理解了所有这些步骤,那就很简单了。总结起来就是这样:
最初的手动过程:
- 编写测试,创建一个运行测试的 HTML 页面;
- 在一两个本地浏览器中运行测试,确保正常工作。
添加自动化:
- 新建 Sauce Labs 账号,获取用户名 和 access key;
- 更新测试页面的源码,使 Sauce Labs 能够通过全局变量读取测试结果;
- 使用 ngrok 创建安全信道连接本地测试页面,使其能公开访问;
- 列出测试的浏览器/平台列表,调用 Start JS Unit Tests 方法;
- 周期调用 Get JS Unit Test Status 直至测试结束;
- 报告测试结果。
更简单的方式
我知道本文开始的时候,我说了很多,你不需要任何框架来完成自动化、跨浏览器的 JavaScript 单元测试,我仍然坚信这一点。但尽管上述步骤很简单,你可能也并不想每个项目都重复去做。
我之前也有很多想要加入自动化测试的老项目,因此对我来说,将这些逻辑打包成模块是很有意义的。
强烈建议你自己尝试去实现,这样才能全面了解它如何工作。但如果你没有时间,且希望能快速搭建测试,建议你尝试我创建的 Easy Sauce 项目。
Easy Sauce
Easy Sauce 是一个 Node 包,一个命令行工具(easy-sauce
)。它也是我现在使用的跨浏览器测试工具。
easy-sauce
命令需要一个 HTML 测试文件的路径(默认为 /test/
),一个启动本地服务器的端口(默认为 1337
),以及以个测试的浏览器/平台列表。接下来 easy-sauce
会在 Sauce Lab 云端运行测试,将结果打印到控制台上并退出,返回合适的状态码,标明测试是否通过。
为了方便 npm 包,easy-sauce
默认会从 package.json
中读取配置项,不用单纯储存它们。这样有个额外好处就是,使用你的包的用户,能够很清楚地看到你所支持的浏览器/平台。
关于 easy-sauce
的完整使用指南,请移步 Github 上的文档。
最后,我想强调的是,这个项目是专门来解决的我的用例的。虽然我觉得它可能对很多其他开发者会很有用,但我无意将其调转为功能齐全的测试解决方案。
easy-sauce
的整个点在于,填补了我(我相信很多开发者也是如此)与合适的测试之间的复杂性裂缝。
总结
本文开始的时候,我写了一个需求列表。有了 Easy Sauce,现在我可以满足所有我工作项目上的需求。
如果你的项目还没用上自动化跨浏览器单元测试,我想鼓励你尝试下 Easy Sauce。就算你不使用它,至少你得有相关知识完成自己的解决方案,或者对现有工具有更好的理解。
测试愉快!
脚注:
1. 使用模板打包工具的一个缺点是堆栈跟踪(stack traces)目前对 source map 的支持还不是很好。Chrome 的一个解决办法是 node-source-map-support 模块。
2. ngrok 生成的 URL 是公开的,这意味着理论上网络上的任何人都可以访问。不过,URL 是随机生成的,而测试只会跑那么几分钟,某人发现它的几率相当低。虽然没有 Sauce Connect 那么安全,相对也还是安全的。