11月阅读周·编写可测试的JavaScript代码:运行客户端JavaScript单元测试篇

举报
叶一一 发表于 2024/11/22 11:28:48 2024/11/22
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》

当前阅读周书籍编写可测试的JavaScript代码

运行客户端JavaScript单元测试

一旦完成了一组测试用例的编写,并将其事件聚合到测试套件中,那接下来的工作呢?在浏览器中加载HTML代码来运行这些测试,在开发代码时这种方式很有趣,并且也很有必要,但是它不适合自动化。如下有一些策略,可以让测试作为自动化构建过程的一部分。

PhantomJS

PhantomJS(https://phantomjs.org/)是headless模式的WebKit浏览器——也就是说,它是一个可以通过编程方式访问的浏览器。这是一个非常不错的沙盒,无需启用浏览器就可以运行单元测试。注意,单元测试通常只专注于从UI中抽象出的核心功能。很显然,PhantomJS并不能、也不会尝试处理不同浏览器上实现的各种各CSS样式(尽管绝大对数都遵循CSS标准)。对CSS或特定于某个浏览器的CSS进行测试,我们不清楚单元测试是否是这种测试类型的最有效方式。也就是说,PhantomJS / WebKit能够处理大量的现代功能;只有最新的CSS 3D转换、本地存储、WebGL这三种功能不被支持。当然,支持什么,完全取决于构建PhantomJS时的WebKit版本,这些版本是不断发展的,并不断添加新的功能。

PhantomJS不再需要X服务器的运行,所以设置变得非常容易!下载适用于操作系统的二进制版本,就可以进行安装了。也可以从源代码编译phantomjs,但要准备好等待一段时间,因为大部分的Qt也需要编译。与此相比,Phantomjs的二进制版本对Qt的依赖最小。

如下是用于对sum.js进行测试的复杂JavaScript文件:

YUI.add('sum', function (Y) {
  Y.MySum = function (a, b) {
    return a + b;
  };
});

设置完以后,利用一个小技巧,就可以非常容易地通过PhantomJS运行YUI测试。如下是对单个加法的sum进行测试所需要的HTML:

<html>
  <head>
    <title>Sum Tests</title>
  </head>
  <body class="yui3-skin-sam">
    <div id="log"></div>
    <script src="http://yui.yahooapis.com/3.4.1/build/yui/yui-min.js"></script>
    <script src="sum.js"></script>
    <script src="phantomoutput.js"></script>
    <script src="sumtests.js"></script>
  </body>
</html>

在这里,加载了另外一个JavaScript文件phantomOutput.js,该文件定义了一个小的YUI phantomjs模块:

YUI.add('phantomjs', function (Y) {
  var TR;
  if (typeof console !== 'undefined') {
    TR = Y.Test.Runner;
    TR.subscribe(TR.COMPLETE, function (obj) {
      console.log(Y.Test.Format.JUnitXML(obj.results));
    });
  }
});

该模块的唯一目的就是在测试完成后,以JUnit的XML格式将测试结果输出至控制台(可以使用YUI支持的其他输出格式,并使用任何可以支持该格式的构建工具——例如,Hudson/Jenkins支持JUnit XML)。这种依赖文件必须在测试文件中进行声明。

PhantomJS会记录控制台输出,并将其保存到文件中,以便稍后处理。然而,在测试中包含phantomjs模块并不是很理想。

PhantomJS并没有访问加载页面上所允许的JavaScript,所以我们利用console控制台,将正在重新加载的页面上的测试结果输出传递给PhantomJS,在这里可以进行持久化。

如下是我在Mac电脑上,对JUTE存储库里的Toolbar示例模块进行测试所做的全部事情:

phantomjs ~/phantomOutput.js sumTests.html

保存快照,以便可以知道到底发生了什么,很简单。如下代码是添加了快照支持的整个脚本。唯一的区别是在控制台输出之后,我们将输出进行了“渲染(render)”:

var page = new WebPage();
page.viewportSize = { width: 1024, height: 768 };

page.onConsoleMessage = function (msg) {
  console.log(msg);
  setTimeout(function () {
    page.render('output.png');
    phantom.exit();
  }, 500);
};

page.open(phantom.args[0], function (status) {
  // check for page load  success
  if (status !== 'success') {
    console.log('unable to load file');
    phantom.exit(1);
  }
});

首先,我设置了视口(viewport)的大小,在得到测试结果后,将页面渲染到一个PNG文件中(该步骤需要包装在setTimeout函数中,以便在退出之前给渲染步骤一点时间)。该脚本在每个测试结束后都会生成一个快照。PhantomJS还可以渲染PDF文件和JPG文件。

在自动化构建过程中,PhantomJS是一个运行单元测试的很好的方法。在构建期间,可以为PhantomJS维护一个文件/URL列表,以便在WebKit浏览器中执行。但是,在将代码发布到生产环境之前,也应该在“真实的”浏览器中运行一下单元测试。接下来我们将看一下如何来做。

Selenium

使用Selenium Remote Control(RC)或Selenium2(WebDriver),可以在真实的浏览器上运行单元测试:浏览器在本地机器上运行或在远程运行。在安装Firefox、Safari、Chrome或IE的机器上运行Selenium JAR,可以很容易地在该浏览器上运行单元测试,然后捕获结果并保存在本地。Selenium2 / WebDriver是Selenium提供的首选/当前工具,如果刚开始用Selenium的话,不要选择Selenium RC。

Selenium是如何工作的呢?在需要运行测试的已安装浏览器上开启Selenium。要完成这个过程,需要到SeleniumHQ网站(https://seleniumhq.org/download/)下载最新版本的Selenium JAR。还需要最新版本的Selenium服务器,撰写本文时其最新版是2.28。然后安装:

java -jar ~/selenium-server-standalone-2.28.0.jar

上述代码,将在默认端口4444上启动Selenium服务器。不管在哪个机器上运行Selenium客户端,都需要链接到该端口,所以要确保防火墙对该端口开放。可以使用-port选项改变该端口。

至此,Selenium服务器就启动并运行了,需要告诉它打开一个浏览器,并获取一个URL。这意味着,需要某个地方的一个Web服务器运行需要测试的文件。它不需要很奇特——记住,这只是单元测试而已,不用架起整个应用程序。然而,我们很容易将测试代码与生产环境代码放在同一个文档根目录下,以简化开发过程中的测试工作。请记住,可能我们不希望将测试文件发布到生产环境中。因此,一个很好的方式是,在根目录上放一个test目录,与此同时放置一个包含生产环境代码的子目录,使所测试模块的目录层级和生产环境的目录层级保持一致。发布生产环境代码时,不用包含test目录。如下图:

01.jpg

test目录树和src目录树一致。test层级里的每片叶节点都(至少)有两个文件:JavaScript测试文件以及与测试关联的HTML文件。

test_user_view.html文件,该HTML文件使用相对路径放置需要测试的user_view.js文件/模块。当本地Web服务器启动时,该文件里的所有本地文件都被找到,接着运行测试。

至此,还需要对Selenium提供一个URL,以便Selenium控制的远程浏览器可以加载URL,并运行我们的测试。使用webdriverjs(https://github.com/Camme/webdriverjs)的Node.js npm包,我们可以很容易地将URL发送给Selenium服务器进行执行:

var webdriverjs = require('webdriverjs'),
  url = '....';
browser = webdriverjs.remote({
  host: 'localhost',
  port: 444,
  desiredCapabilities: { browsername: 'firefox' },
});
browser.init().url(url).end();

上述代码将链接运行在localhost的4444端口上的Selenium服务器,告诉它启动Firefox并加载指定的URL。至此,工作已经完成一半了!我们现在需要做的是捕获测试输出——YUI Test发出的这些有用的日志消息——捕获快照,并本地保存。

如果使用PhantomJS,我们需要以某种方式将所有的输出(测试结果、日志消息和快照数据)都发送给Selenium服务器。与PhantomJS不同,Selenium无法远程读取console对象,所以我们必须应用其他技巧来管理这一通信。

我们将从YUI Test获取到的所有消息都保存在一个数组中,而不是把所要捕获的数据写入到console.log。测试结束后,创建一个指定ID的<div>,将JSON版本的所有消息和结果放在该<div>中。最后,将该新元素附加到当前文档中。这其中有两个最重要的点是:<div>必须是可见的,我们必须将整个内容生成的JSON字符串进行编码(escape),因为JUnit XML输出的是非常怪异的XML。我们不希望浏览器尝试解析它;否则它就会出错,因为它不是有效的HTML。

要重新获取XML,我们要做的第一件事就是对文本内容进行解码(unescape)。现在,可以重建包含messages属性和results属性的散列对象了,该messages属性是YUI Test消息数组,而results属性则包含该测试运行所产生的JUnit XML输出。

使用该运行器,可以用命令行在真实的Firefox、Chrome、IE浏览器上运行测试,只要运行的Selenium服务器,和其中一个浏览器运行在同一个主机上即可。

最后,怎么保存快照呢?Selenium很方便地提供了快照支持。让我们来看一些图片!与PhantomJS不同,简单的Selenium运行器只知道测试何时结束,所有我们就在这个时候捕获快照。

使用Selenium有一个速度的问题:为每个测试或测试套件都启动一个浏览器会话是一个非常缓慢的过程。可以关注一下Ghost Driver项目(https://github.com/detro/ghostdriver/)。该驱动程序与Chrome驱动程序类似,允许Selenium使用PhantomJS作为后端。当然,这大大加快了Selenium后端启动和卸载的时间。在撰写本文时,该项目并不支持所有的Selenium元素,但大多数基本的都支持。如果Selenium测试的速度对你来说很重要,就应该使用该项目!

总结

加速Selenium测试的另一个选择是使用Selenium网格将测试并行运行在多个运行器上。


作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。