At the Forge - 测试 JavaScript
在 2009 年 11 月中旬,在芝加哥的研究小组会议上,我自豪地展示了我的博士论文软件的最新 beta 版本,这是一个 Web 应用程序(使用 Ruby on Rails 编写),旨在促进学生和科学家之间的协作。我非常有信心测试不会发现太多技术问题,部分原因是使用了 Cucumber 和 rcov 来确保高程度的测试覆盖率。诚然,我的应用程序使用了一些 AJAX,这意味着 Cucumber 无法测试某些内容。但是,考虑到这些功能是多么本地化,并且我自己在日常工作中使用了它们并进行了测试,这有多重要呢?
好消息是,在很大程度上,beta 测试进行得非常顺利。有一些问题需要修复,我开始制定一个计划来解决这些问题。最困扰我的不是 bug 的存在,而是 bug 都与 JavaScript 和 AJAX 相关。换句话说,我实现的高水平测试覆盖率很好,但还不够充分,因为它只关注了我的 Ruby 代码,而没有关注我的应用程序中同样重要的 JavaScript 代码。
这并不是我第一次遇到 JavaScript 测试的问题。我在 2009 年的大部分时间里参与的一个项目使用了大量的 JavaScript,我们尝试了多种方法来测试它,但没有一种方法特别令人满意。
因此,我惊喜地发现,我并不是唯一一个试图提高包含大量 JavaScript 的 Web 应用程序的测试覆盖率的 Web 开发人员。实际上,目前有许多框架和库可用于 JavaScript 测试——其中一些特定于特定的 JavaScript 框架,一些是 Ruby on Rails(或其他 Web 应用程序框架)的插件,还有一些相当灵活且不可知。
本月,我将介绍 Screw.Unit,这是一个我已开始在自己的工作中使用的 JavaScript 测试框架。即使您不专门使用 Screw.Unit,现代 Web 开发人员也必须不断考虑编写可测试代码的方法,不仅在他们选择的服务器端语言中,而且在 JavaScript 中也是如此。JavaScript 在现代 Web 应用程序中扮演着核心角色,未能彻底测试它可能会导致无法预见的问题,正如我亲眼所见。
Screw.Unit 最初由 Nick Kallen(来自 Pivotal Labs)编写,并在 GitHub 上以开源形式发布。存在许多派生版本,您可能需要仔细查找才能找到一个足够主流和现代的版本来满足您的需求。我一直在使用 Kallen 的原始版本,并在本文的示例中依赖它。GitHub 提供了多种下载软件的方法,但最简单的方法是使用以下命令“克隆”现有的 Git 存储库:
git clone git://github.com/nkallen/screw-unit.git
在 screw-unit 目录中,您将找到许多 JavaScript 库和 CSS 文件,所有这些文件都旨在帮助您运行 JavaScript 测试。
Screw.Unit 的基本思想是,您使用 describe() 引入一组相关的测试,然后使用 it() 引入每个单独的测试。it() 的第二个参数是一个函数,该函数使用定义的 expect() 函数调用一个或多个断言。
因此,假设您定义了一个函数,该函数将其参数乘以 3
function triple(i) { return i * 3; }
您可以使用以下 Screw.Unit 代码对其进行测试
describe("Triple should triple", function() { it("returns 6 for 2", function() { expect(triple(2)).to(equal, 6); }); });
请注意此处涉及的三个单独的函数级别
describe引入一个通用规范块。
it描述并引入一个单一规范。
expect为该规范执行一个测试。
为了运行这些测试,您需要将整个 describe 块包装在一个匿名函数中,并将其作为 Screw.Unit() 的第一个参数传递
Screw.Unit(function() { describe("Triple should triple", function() { it("returns 6 for 2", function() { expect(triple(2)).to(equal, 6); }); }); });
最后,您需要引入一堆 JavaScript 库,这些库不仅定义了 Screw.Unit,还定义了它所依赖的对象和函数。最终版本如清单 1 triple.html 所示。请注意,虽然您正在测试 JavaScript,但 Screw.Unit 假设您在 HTML 文件中执行此操作。这使您不仅可以加载(不幸的是很长的)JavaScript 库列表,还可以加载 Screw.Unit 中用于显示测试结果的 CSS 文件。
清单 1. triple.html
<html> <head> <script src="lib/jquery-1.2.6.js"></script> <script src="lib/jquery.fn.js"></script> <script src="lib/jquery.print.js"></script> <script src="lib/screw.builder.js"></script> <script src="lib/screw.matchers.js"></script> <script src="lib/screw.events.js"></script> <script src="lib/screw.behaviors.js"></script> <link rel="stylesheet" href="lib/screw.css"> <!-- Here is the function we define, to test --> <script type="text/javascript"> function triple(i) { return i * 3; } </script> <!-- Here is the test itself --> <script type="text/javascript"> Screw.Unit(function() { describe("Triple should triple", function() { it("returns 6 for 2", function() { expect(triple(2)).to(equal, 6); }); }); }); </script> </head> <body> </body> </html>
当执行 Screw.Unit() 时,测试通过。如果运行良好,HTML 文档的主体将相应地进行修改,使用 CSS 类(在 screw.css 中定义),这些类为执行的测试提供传统的绿色(表示通过)或红色(表示失败)报告。
我将添加另外两个测试,一个我知道会通过的测试,它使用 not_equal 测试修饰符。另一个测试将会失败,这样您就可以检查一个测试失败时的样子。如果一切顺利,您应该看到两个绿条和一个红棕色条,最后一个表示失败。测试本身如下所示
<script type="text/javascript"> Screw.Unit(function() { describe("Triple should triple", function() { it("returns 6 for 2", function() { expect(triple(2)).to(equal, 6); }); it("does not return 100 for 2", function() { expect(triple(2)).to_not(equal, 100); }); it("does return 100 for 2 -- fail!", function() { expect(triple(2)).to(equal, 100); }); }); }); </script>
如您所见,您可以在一个 describe 块中包含任意数量的 it 语句。随着时间的推移,您将看到您的规范增长,包含越来越多的描述、it 语句和 expect 语句。
测试 JavaScript 函数当然是一件好事。但是,JavaScript 的主要用途之一是修改 DOM——文档对象模型,它提供了对 HTML 页面内容的句柄。使用 DOM,您可以查看或修改页面,包括其标签和内容。
因此,DOM 操作是 JavaScript 测试的主要目标。您希望能够说,当单击 HTML 的特定部分时,应出现另一部分 HTML。
现在,Screw.Unit 的一些文档会告诉您,您可以(并且应该)使用 click() 方法来模拟单击页面上的元素。我不仅发现 click() 方法不可靠,而且还被 Screw.Unit 邮件列表上的帖子说服,将我的文本隐藏代码放在一个单独的函数中,然后可以从段落的 click() 处理程序以及 it 块中的测试中调用该函数。在我看来,这不仅奏效了,而且还鼓励了一种更易读且易于操作的风格。
清单 2. clickview.html
<html> <head> <script src="lib/jquery-1.2.6.js"></script> <script src="lib/jquery.fn.js"></script> <script src="lib/jquery.print.js"></script> <script src="lib/screw.builder.js"></script> <script src="lib/screw.matchers.js"></script> <script src="lib/screw.events.js"></script> <script src="lib/screw.behaviors.js"></script> <link rel="stylesheet" href="lib/screw.css"> <!-- Here is the function we define, to test --> <script type="text/javascript"> function hide_paragraph() { $("#hideme").hide(); } $(document).ready(function() { $('#hideme').click(function() { hide_paragraph(); }); }); </script> <!-- Here is the test itself --> <script type="text/javascript"> Screw.Unit(function() { describe("Paragraph", function() { it("should be hidden when clicked", function() { hide_paragraph(); expect($('#hideme').is(':hidden')).to(equal, true); }); }); }); </script> </head> <body> <p id="hideme">Click to hide</p> </body> </html>
完整文件 clickview.html 在清单 2 中。其思想是文档包含一个段落
<p id="hideme">Click to hide</p>
然后,您将 click() 事件处理程序附加到该段落,在单击该段落时调用一个函数
function hide_paragraph() { $("#hideme").hide(); } $(document).ready(function() { $('#hideme').click(function() { hide_paragraph(); }); });
最后,您设置一个 Screw.Unit() 测试块,如下所示
Screw.Unit(function() { describe("Paragraph", function() { it("should be hidden when clicked", function() { hide_paragraph(); expect($('#hideme').is(':hidden')).to(equal, true); }); }); });
当您加载页面时,Screw.Unit 首先调用函数 hide_paragraph(),其效果与单击它相同。然后,它使用伪类 (:hidden) 检查以确保隐藏了文本,以识别隐藏文本。如果当前没有 ID 为 “hideme” 的文本被隐藏,则 jQuery 返回一个空列表,并且断言失败。
事实上,Screw.Unit 和 jQuery 中的所有内容都是使用 CSS 选择器完成的,这使得它易于快速使用。似乎有人在使用 Screw.Unit 进行 TDD(测试驱动开发)和 BDD(行为驱动开发);虽然我不认为自己是其中之一,但我确实看到自己在未来使用这种测试,即使只是为了避免在我的软件用户面前丢脸。此外,至少在我看来,以这种方式测试 JavaScript 让我感觉是在开发严肃的软件,而不是一个或多个基本的 hack。
我应该注意到,我在本专栏中介绍 Screw.Unit 的风格是一种简洁但不符合习惯的方式,大多数用户会将其付诸实践。Screw.Unit 用户通常将测试分为多个文件,这不仅允许他们定义自定义测试匹配器,而且还拥有一个完整的测试库,而不仅仅是一个文件。一旦您开始使用 Screw.Unit,我相信您会找到一种适合您需求的风格,而又不会过多地违反系统的预期用法。
Screw.Unit 是一个易于理解、易于使用的框架,用于测试您的 JavaScript 代码。它不是唯一的同类测试系统,但它的语法让人联想到 RSpec,这使得像我这样喜欢并使用 RSpec 的人更容易快速上手。RSpec 的拥护者也会希望我指出,Screw.Unit 为 JavaScript 开发人员提供了与 RSpec 和 Cucumber 相同的 BDD,专注于用户看到的结果,而不是内部工作原理。
如果您以前从未测试过您的 JavaScript,那么现在就是开始的最佳时机!至少,您要确保单击 HTML 页面的各个部分不会导致错误。
资源
Screw.Unit 主要版本的首页位于 GitHub,网址为 github.com/nkallen/screw-unit。该页面上的文档有些稀疏,但它提供了几个关于如何创建和使用 Screw.Unit 测试的示例。
Screw.Unit 的小型教程也托管在 GitHub 上,网址为 github.com/bsiggelkow/screw-unit-tutorial。本教程使用 Sinatra 框架进行 Web 应用程序开发,因此您需要拥有 Ruby 的副本和用于 Ruby 的 Sinatra gem 才能开始使用。
Screw.Unit 用户有一个电子邮件列表,您可以在 groups.google.com/group/screw-unit 订阅。
一篇博客文章,基于电子邮件消息,该消息向我展示了不使用 click() 方法的重要性,并更详细地描述了如何编写更好的测试,可在 blog.runcoderun.com/post/177871245 上找到。
jQuery 是 Screw.Unit 中使用的 JavaScript 库,您可能希望为自己的浏览器内应用程序探索它,网址为 jquery.org。
Reuven M. Lerner 是一位长期的 Web/数据库开发人员和顾问,是西北大学学习科学博士候选人,研究在线学习社区。他在芝加哥地区居住了四年后,最近(与他的妻子和三个孩子)返回了他们在以色列莫迪因的家。