用 Dojo Objective Harness 对 Web 2.0 应用程序进行单元测试

 
 
 

级别: 中级

Jared Jurkiewicz, 顾问软件工程师, IBM
Stephanie L. Walter, 顾问软件工程师, IBM

2008 年 11 月 25 日

单 元测试是保证软件开发质量的一个重要部分,对于敏捷和极限编程开发方法尤其如此。通常,对 Web 2.0 客户端用户界面进行自动的单元测试很困难,所以很少有人去做尝试。然而,Dojo 提供了一个单元测试工具,借此可以评估 JavaScript 的功能及用户界面的可视性。经过这个工具彻底测试过的用户界面最终包含的 Bug 数量会极大的减少。本文阐述了 Dojo Objective Harness (DOH) 的主要特点并通过与其它 Web 2.0 应用程序测试工具的比较展示了其强大的功能。

单元测试用例

编写单元测试通常是为了测试一段源代码。理论上讲,这个代码片段(或者说是代码单元)是源代码中最小的可测试部分。一个单元测试通常是自动进行的,但也不一定必须自动执行,单元测试的结果表明这段代码是否能按照设计的要求工作。

众所周知,软件开发人员在时间方面通常都很紧张。为了将产品尽快投入市场,他们要面临不小的压力,那么为什么还要在编写单元测试上花费更多时间呢?这是因 为一个充分的单元测试套件不仅能产生高质量的代码,并且由于减少了调试 Bug 的时间而最终节省大量时间。另外,如果能依照敏捷开发方法在编写源代码前先编写单元测试,还会减少所需编写的代码。如果在开始编写代码前先对设计进行全面 细致的考虑,也能减少您为实现单元测试的目的而需要编写的代码量。

 




回页首

 

什么是 Dojo Objective Harness?

单元测试有众多的支持者,正如在极限编程以及敏捷编程中看到的那样。Asynchronous JavaScript + XML (Ajax) 及 Web 2.0 用户界面的广泛使用催生了对客户端单元测试的需求。Dojo Objective Harness 是 Web 2.0 UI 开发人员用于 JUnit 的工具。与已有的 JavaScript 单元测试框架(比如 JSUnit)不同,DOH 不仅能够实现在使用或不使用 Dojo 的情况下自动处理 JavaScript 函数,它还可以对用户界面的可视性 进行单元测试。这是因为 DOH(多好的缩写名)既提供了命令行界面,也提供了基于浏览器的界面来测试框架。

 




回页首

 

浏览器和非浏览器环境

前面提到过,DOH 既提供命令行界面,又提供了基于浏览器的界面。如果单元测试需要完全自动化,并且不需要可视组件,那么命令行界面是个不错的选择,这是因为它可通过一个构 建脚本启动,且其结果可被记录。此外,这个界面还提供了一个与 JUnit 非常相似的单元测试环境。DOH 为其命令行界面还使用了 Rhino,一个用 Java™ 代码编写的开源 JavaScript 引擎。正因如此,对 documentwindowDOMParserXMLHttpRequest 对象的引用无法被解析。Rhino 的另一个问题是它使用了一个与一般浏览器不同的 JavaScript 解释程序,这使得测试有可能在一个运行时内通过,而在另一个运行时内则不能。

如果单元测试需要可视组件和访问各种 JavaScript 对象,那么基于浏览器的界面将是最佳选择。需要提醒您的是使用浏览器的单元测试并不是 100% 自动的;您必须在自己衷爱的浏览器中启动单元测试并要检查其结果。其实这并不意外。一个 UI 外观的好坏通常是人的主观判断。浏览器测试的运行程序提供了两个途径来显示测试结果:一个是可视化结果,另一个是单元测试统计数据。图 1 在左侧显示了运行的测试用例,而在右侧 Test Page 选项卡下则可视化显示了代码执行(单击 这里 可以看到图 1 的放大图)。


图 1. DOH 单元测试可视化
DOH 单元测试可视化

图 2 显示了在 Log 选项卡下的单元测试统计数据(单击 这里 可以看到图 2 的放大图)。


图 2. DOH 单元测试统计数据
DOH 单元测试统计数据




回页首

 

浏览器的兼容性

针 对多种浏览器和版本开发过客户端代码的人都知道,要能通过单元测试快速检测到浏览器行为的差别,这一点非常重要。因为 DOH 测试运行程序是 HTML 和 JavaScript,所以单元测试可以在任何浏览器中执行。这就意味着您可以在 FireFox、Internet Explorer 和 Safari (及它们的不同版本)中运行测试并比较各自的结果。您不仅可以确保基本 JavaScript 方法在各种平台中都有相同的表现,而且还可以确保可视化在各种平台中也是相同的,或至少差不多。我们都知道一个小部件在一个浏览器可能表现良好,但在其他 浏览器中就不一定了。跨浏览器的 bug 通常很令人讨厌且很难被修复。若能提前自动地测试浏览器的兼容性,就可以在软件投入市场前及时发现和修复跨浏览器支持的问题。

 




回页首

 

可用的测试函数

每个测试框架都要为开发人员提供检查单元测试结果的方法,DOH 也不例外。DOH 提供了 3 个可在测试验证中使用的断言 API,如清单 1 所示。


清单 1. 3 个断言 API

     doh.assertEqual(expectedResult, actualResult)
doh.assertFalse(testCondition)
doh.assertTrue(testCondition)

 

此外,还可以使用这 3 个函数的简化版。清单 2 显示了这些版本。


清单 2. 断言 API 的简化版

     doh.is(expectedResult, actualResult)
doh.f(testCondition)
doh.t(testCondition)

 

当断言失败时,就会抛出一个异常。如果在一个单元测试中有任 何类型的异常被抛出,DOH 就会宣布整个测试失败。在预料到测试会抛出异常时,这一点很重要。在这种情况下,需要用一个 try catch 程序块来包围代码。当调用单元测试时,DOH 就会报告所有已发生的错误及失败的特定测试。DOH 还会报告测试运行、已发生的错误以及失败测试的总数。

在编写单元测试时,最好把断言的数量控制在最小,因为借助 DOH 错误报告机制,很难判断失败是由哪个断言引起的。尽管通常判断失败由哪个 equals 断言引起相对比较容易,但断言的真假却较难判断。

有时,发生在单元测试中的错误不是由断言抛出的。如果是这样,不是单元测试有问题,就是被测试的代码不正确。幸运的是,Firefox 的 Firebug 插件 可被用来调试单元测试中的基础代码问题。

 




回页首

 

异步函数测试

若能对客户端应用程序发出的异步调用的行为进行单元测试,岂不是很棒?DOH 可以帮得上这个忙。测试 Ajax 请求的行为是 DOH 最有价值的特点之一。因为借助其基于浏览器的界面可以访问 XMLHttpRequest 对象,所以 DOH 可以支持异步单元测试。要指示一个测试用例是异步的,此测试用例需要通过返回一个 doh.Deferred 对象来提示 DOH。如果 DOH 不知道这个测试是异步的,那么在此测试的代码执行之后,DOH 就会认为此测试已完成,没有错误发生。显然,这将导致测试成功的假象,而且还会使得部分代码得不到测试。

必须要在了解异步上下文的基础上对这个测试示例本身进行编写。当从单元测试中返回一个 doh.Deferred 对象时,必须捕获异步调用中产生的所有错误信息并把它们传递给对象的 errback 方法。如果没有异常发生,就应该用一个真值参数调用这个对象的 callback 方法。这能使 DOH 准确地报告失败的测试。

为了使编写异步测试变得简单,doh.Deferred 对象提供了一个 getTestCallback 函数来隐式地处理在异步调用的回调函数中发生的异常。您只需将测试函数传递给 getTestCallback,而它反过来包含了所想执行的断言。这能让您不用再手工处理异步调用过程中发生的异常。更多信息,请参见 编写自已的测试套件 一节。

DOH 还允许以毫秒为单位设定超时值,一旦响应没有在指定的时间内返回,测试就会失败。异步测试的默认超时值是 500ms,也就是半秒,所以,很多时候,最好是显式地指定一个更长的超时值,这样一来,测试就不会失败。

 




回页首

 

编写自已的测试套件

用 DOH 编写自已的测试套件初看上去很复杂,但实际上它并不难。DOH 框架对如何定义和加载测试的要求很灵活,通常可以修改加载流程以适合您具体的结构。Dojo 的单元测试几乎都遵循通用的结构以使新模块所有者便于上手和使用。建议您在熟练掌握 DOH 工作原理之前,最好遵循现有的约定。

 




回页首

 

DOH 测试用例的基本结构

通过一个示范模块 demo.doh,可以说明测试用例结构,该模块作为一个 Dojo 目录结构的对等模块。之所以采用对等结构是因为 DOH 框架使用 Dojo 的模块加载程序结构,并且没有用 dojo.registerModulePath() 告知 Dojo 源代码在什么位置,它假定模块目录是 Dojo 的对等目录。然而,这可以按如下方式得到解决:编辑 util/doh/runner.html 来注册模块路径,若再能提前导入 doh.runner,将会使初级用户很容易就能遵循 Dojo 的约定。图 3 显示了这个通用的目录结构,该结构会在本节中多次提到。


图 3. 通用的目录结构
通用的目录结构

如图 3 所示,让每个 Dojo 模块都包含只针对该模块的单元测试是个很好的做法。这使模块开发者能够在独立于整个项目的情况下运行单元测试。但这不意味着不允许任何能够加载所有模块的 全部单元测试的测试套件模块文件的存在。有关内容会在详细介绍完此结构的基础知识后,在本文后面的章节给出。

 




回页首

 

一组 DOH 的测试用例

在我们开始进行测试并探讨其工作原理之前,了解所测试的对象将会很有帮助。在 demo.doh 的示例中,测试的对象是一个模块,它包含帮助函数和一个简单 DemoWidget。之所以要包含这两者是因为它们能有效地说明如何测试不可视的 JavaScript 函数,以及如何像测试应用程序中的小部件一样测试直接用于 HTML 中的小部件。为了便于理解,这些文件所实现的行为很简单。清单 3 显示了 demoFunctions.js 的内容,清单 4 显示了 DemoWidget.js 的内容。


清单 3. demoFunctions.js 的内容

dojo.provide("demo.doh.demoFunctions");

//This file contains a collection of helper functions that are not
//part of any defined dojo class.

demo.doh.demoFunctions.alwaysTrue = function() {
// summary:
// A simple demo helper function that always returns the boolean true when
// called.
// description:
// A simple demo helper function that always returns the boolean true when
// called.
return true; // boolean.
};

demo.doh.demoFunctions.alwaysFalse = function() {
// summary:
// A simple demo helper function that always returns the boolean false when
// called.
// description:
// A simple demo helper function that always returns the boolean false when
// called.
return false; // boolean.
};

demo.doh.demoFunctions.isTrue = function(/* anything */ thing) {
// summary:
// A simple demo helper function that returns true if the thing passed in is
// logically true.
// description:
// A simple demo helper function that returns true if he thing passed in is
// logically true.
// This means that for any defined objects, or Boolean values of true, it
// should return true,
// For undefined, null, 0, or false, it returns false.
// thing:
// Anything. Optional argument.
var type = typeof thing;
if (type === "undefined" || thing === null || thing === 0 || thing === false) {
return false; //boolean
}
return true; // Boolean
};

demo.doh.demoFunctions.asyncEcho = function(/* function */ callback,
/* string */ message){
// summary:
// A simple demo helper function that does an asynchronous echo
// of a message.
// description:
// A simple demo helper function that does an asynchronous echo
// of a message.
// The callback function is called and passed parameter 'message'
// two seconds
// after this helper is called.
// callback:
// The function to call after waiting two seconds. Takes one
// parameter,
// a string message.
// message:
// The message to pass to the callback function.
if (dojo.isFunction(callback)) {
var handle;
var caller = function() {
callback(message);
clearTimeout(handle);
handle = null;
};
handle = setTimeout(caller, 2000);
}
};



清单 4. demo/doh/DemoWidget.js 的内容

dojo.provide("demo.doh.DemoWidget");
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");

dojo.declare("demo.doh.DemoWidget", [dijit._Widget, dijit._Templated],

//The template used to define the widget default HTML structure.
templateString: '<div dojoAttachPoint="textNode" style="width: 150px; ' +
' margin: auto; background-color: #98AFC7; font-weight: bold; color: ' +
'white; text-align: center;"></div>',

textNode: null, //Attach point to assign the content to.

value: 'Not Set', //Current text content.

startup: function() {
// summary:
// Overridden startup function to set the default value.
// description:
// Overridden startup function to set the default value.
this.setValue(this.value);
},

getValue: function() {
// summary:
// Simple function to get the text content under the textNode
// description:
// Simple function to get the text content under the textNode
return this.textNode.innerHTML;
},

setValue: function(value) {
// summary:
// Simple function to set the text content under the textNode
// description:
// Simple function to set the text content under the textNode
this.textNode.innerHTML = value;
this.value = value;
}
});

 




回页首

 

在 DOH 中同步和异步地测试独立函数

如清单 3 和 4 所示,我们已经实现了一个简单的小部件和少许独立函数。既然它们已经被定义完毕,我们不妨来实施单元测试来执行函数及小部件以确保它们能像预期的那样运行。对于其他 JavaScript 单元测试框架而言,同步函数很容易测试,但异步函数 demo.doh.demoFunctions.asyncEcho 和小部件的测试就不那么容易了。因此,需要借助 DOH 来处理浏览器内的小部件测试及异步函数测试。

最简单的着手点是测试独立函数。编写独立函数测试用例就像定义 JavaScript 数组一样简单。这个数组应包含测试函数、测试装置(fixture)或同时包含两者。使用哪一个依测试的复杂程度而定。在大多数情况下,简单的测试函数对 测试代码来说已经足够了。只有在需要更改超时值、执行设置操作或在测试后要拆除数据时,才需要构造一个测试装置。在定义了函数数组后,若要在 DOH 中对之进行注册,只需用两个参数调用 tests.register 即可,这两个参数分别为想要分配给测试集合的名称和此测试数组。清单 5 是用于 demoFunctions.js 独立函数的一组测试的代码清单。


清单 5. demo/doh/tests/functions/demoFunctions.js 的内容

dojo.provide("demo.doh.tests.functions.demoFunctions");

//Import in the code being tested.
dojo.require("demo.doh.demoFunctions");

doh.register("demo.doh.tests.functions.demoFunctions", [
function test_alwaysTrue(){
// summary:
// A simple test of the alwaysTrue function
// description:
// A simple test of the alwaysTrue function
doh.assertTrue(demo.doh.demoFunctions.alwaysTrue());
},
function test_alwaysFalse(){
// summary:
// A simple test of the alwaysFalse function
// description:
// A simple test of the alwaysFalse function
doh.assertTrue(!demo.doh.demoFunctions.alwaysFalse());
},
function test_isTrue(){
// summary:
// A simple test of the isTrue function
// description:
// A simple test of the isTrue function with multiple permutations of
// calling it.
doh.assertTrue(demo.doh.demoFunctions.isTrue(true));
doh.assertTrue(!demo.doh.demoFunctions.isTrue(false));
doh.assertTrue(demo.doh.demoFunctions.isTrue({}));
doh.assertTrue(!demo.doh.demoFunctions.isTrue());
doh.assertTrue(!demo.doh.demoFunctions.isTrue(null));
doh.assertTrue(!demo.doh.demoFunctions.isTrue(0));
},
{
//This is a full test fixture instead of a stand-alone test function.
//Therefore, it allows over-riding of the timeout period for a deferred test.
//You can also define setup and teardown function
//for complex tests, but they are unnecessary here.
name: "test_asyncEcho",
timeout: 5000, // 5 seconds.
runTest: function() {
// summary:
// A simple async test of the asyncEcho function.
// description:
// A simple async test of the asyncEcho function.
var deferred = new doh.Deferred();
var message = "Success";
function callback(string){
try {
doh.assertEqual(message, string);
deferred.callback(true);
} catch (e) {
deferred.errback(e);
}
}
demo.doh.demoFunctions.asyncEcho(callback, message);
return deferred; //Return the deferred. DOH will
//wait on this object for one of the callbacks to
//be called, or for the timeout to expire.
}
}
]);

 

如清单 5 所示,定义一组基础测试并不需要太多代码,即便由于更改默认超时值而需要用测试装置来执行测试也是如此。这些测试还显示了编写单元测试的另一种很好的做 法,那就是让测试尽量地简单和小巧。每个测试只有少数几个断言,其原因是这样做能更快地区分出测试失败和 DOH 所报告的错误。太多的断言会使我们很难判断错误是由哪个断言引起的。

关于测试的值得注意的另一点是为什么通常还要编写异步测试。因为回调运行得较晚,所以当故障出现时,DOH 很难通过 try/catch 捕捉到,就如同在同步测试中一样。相反,单元测试必须要考虑到这一点。对于 asyncEcho 测试,它将断言包装进一个 try/catch 程序块,并且,任何错误都将通过 deferred.errback(error) 调用被传递回 DOH。假设没有执行包装,那么测试还将在错误出现时停止,但 DOH 报告的内容却是测试超时。这是因为从这个失败的断言中抛出的错误将会阻止 deferred.callback() 的执行。所以,根据 DOH 的报告,这个测试永远不会完成,只会超时。换句话说,DOH 得知异步测试是通过还是失败的惟一途径就是:操作是否在延迟操作上被调用了。

 




回页首

 

在 DOH 中测试小部件

如 前面的小节所示,测试简单的独立函数很容易做到。只需创建一个函数数组或测试装置、然后对之进行注册,加载后,DOH 就会执行它们。这固然很棒,但独立函数与非可视代码远不是 JavaScript 的全部,它还涉及到用浏览器 DOM 提供更具互交性的观感。所以,接下来要探讨的问题就是如何测试小部件?

还好,DOH 为注册测试提供了一个很好的框架和方法,这些测试一般需要 Web 浏览器加载一个 HTML 文件,该文件用于实例化要测试的小部件。实际上,DOH 所做的就是要在 HTML 文件(iframe 内)内运行的 DOH 的实例和运行其 UI 和独立测试的 DOH 的实例之间建立一座桥梁。这里要记住的是与独立函数测试不同,小部件测试一般不能通过 Rhino 这样的 JavaScript 解释器顺利运行。

那么,怎样定义小部件测试呢?首先定义一个 HTML 文件来实例化此 DOH、小部件,然后定义要执行的测试函数。清单 6 显示了一个 HTML 文件的代码清单,这个 HTML 文件利用 DOH 测试 demo.doh.DemoWidget


清单 6. demo/doh/tests/widgets/DemoWidget.html 的内容

<html>
<head>
<title>DemoWidget Browser Tests</title>
<script type="text/javascript" src="../../../../dojo/dojo.js"
djConfig="isDebug: true, parseOnLoad: true"></script>
<script type="text/javascript">
dojo.provide("demo.doh.tests.widgets.DemoWidgetHTML");
dojo.require("dojo.parser");
dojo.require("doh.runner");
dojo.require("demo.doh.DemoWidget");

dojo.addOnLoad(function(){
doh.register("demo.doh.tests.widgets.DemoWidget", [
function test_DemoWidget_getValue(){
// summary:
// Simple test of the Widget getValue() call.
doh.assertEqual("default", dijit.byId("demoWidget").getValue());
},
function test_DemoWidget_setValue(){
// summary:
// Simple test of the Widget setValue() call.
var demoWidget = dijit.byId("demoWidget");
demoWidget.setValue("Changed Value");
doh.assertEqual("Changed Value", demoWidget.getValue());
}
]);
//Execute D.O.H. in this remote file.
doh.run();
});
</script>
</head>
<body>
<!-- Define an instance of the widget to test. -->
<div id="demoWidget" dojoType="demo.doh.DemoWidget" value="default"></div>
</body>
</html>


 

如清单 6 所示,运行 DOH 的是一个独立文件。这很棒,但它没有显示 DOH 的 UI, 因此,很难断定测试是通过了还是没通过。要是 DOH 能提供一个既能运行 HTML 文件又能显示 UI 的机制就好了。幸运的是,它可以这样做。DOH 有另外一个测试注册函数,名为 doh.registerUrl()。 此函数能让 DOH runner.html UI 指向另一个 HTML 文件。接下来它要做的就是将该 HTML 文件载入框架中,然后将由该 HTML 文件创建的 DOH 实例与 UI 的 DOH 实例相连接,之后此 UI 就能从这个 HTML 页面显示测试失败或成功了!清单 7 显示这个模块文件的代码,它注册一个 URL 作为测试和结果的源。


清单 7. demo/doh/tests/widgets/DemoWidget.js 的内容

dojo.provide("demo.doh.tests.widgets.DemoWidget");

if(dojo.isBrowser){
//Define the HTML file/module URL to import as a 'remote' test.
doh.registerUrl("demo.doh.tests.widgets.DemoWidget",
dojo.moduleUrl("demo",
“doh/tests/widgets/DemoWidget.html"));
}

 




回页首

 

把它们放在一起:将测试定义合并到单个 DOH 测试套件中

至此,您已经看到了如何编写单个测试文件。如示范的那样,编写单个测试并不复杂。所以,剩下的问题就是如何获取这些测试定义、如何将它们加载到 DOH 的 UI 中以及如何执行它们。其实这也不难,只需编写一个重定向到 DOH 的 runner.html 的 HTML 文件即可。作为重定向的一部分,需要传递一个请求参数以定义 JavaScript 模块文件所要载入的内容。这个模块文件,通常被称为 module.js,它使用 dojo.require() 加载每个测试文件。当 dojo.require() 引入这些文件时,也注册了这些测试。当所有测试文件都由 DOH 加载后,此框架就会自动执行这些测试。清单 8 所示的是此重定向文件。清单 9 是引入所有测试文件的 module.js 文件。


清单 8. demo/doh/tests/runTests.html 的内容

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>demo.doh Unit Test Runner</title>
<meta http-equiv="REFRESH"
content="0;url=../../../util/doh/runner.html?testModule=demo.doh.tests.module">
</head>
<body>
Redirecting to D.O.H runner.
</body>
</html>



清单 9. demo/doh/tests/module.js 的内容

dojo.provide("demo.doh.tests.module");
//This file loads in all the test definitions.

try{
//Load in the demoFunctions module test.
dojo.require("demo.doh.tests.functions.demoFunctions");
//Load in the widget tests.
dojo.require("demo.doh.tests.widgets.DemoWidget");
}catch(e){
doh.debug(e);
}

 




回页首

 

结束语

尽管 DOH 对一个新手来说有些复杂,但它的确是一个灵活且强大的单元测试框架。它将测试模块化为可单独加载的文件,并提供函数以将测试合并成组,此外还提供了一系列 测试 API 来断言执行代码的条件,甚至还通过 URL 注册和 iframe 页面加载提供了处理异步测试以及浏览器小部件测试的框架。

通过对 DOH 进行仔细分析,我们发现它并不复杂。编写一个简单的测试用例很快也很容易,把这些测试示例合并成一个套件也只需编写一个 JavaScript 文件即可,其中 dojo.require() 要包括在每组单独的测试中。此模块文件就是测试套件的入口点。DOH 还提供了一个强大的 UI ,可用来显示成功或失败甚至抛出的错误。要想利用它,只需要用一个定义所要加载文件的查询参数加载 runner.html,此文件将用来注册测试。

最后,DOH 不只限于浏览器环境。基础 DOH 加载程序和框架均能用于 JavaScript 环境中,例如 SpiderMonkey 和 Rhino。DOH 的确是测试 JavaScript 代码的最完整和最有效的框架之一。

 

 

Document options
<script type="text/javascript"> <!-- document.write('<tr valign="top"><td width="8"><img src="//www.ibm.com/i/c.gif" width="8" height="1" alt=""/></td><td width="16"><img alt="Set printer orientation to landscape mode" height="16" src="//www.ibm.com/i/v14/icons/printer.gif" width="16" vspace="3" /></td><td width="122"><p><b><a class="smallplainlink" href="javascript:print()">Print this page</a></b></p></td></tr>'); //--> </script>
Set printer orientation to landscape mode

Print this page

PDF format - Fits A4 and Letter

PDF - Fits A4 and Letter
167KB

Get Adobe® Reader®

<script type="text/javascript"> <!-- 5.6 10/24 llk: added cdata around the subdirectory path of email gif--> <!-- document.write('<tr valign="top"><td width="8"><img src="//www.ibm.com/i/c.gif" width="8" height="1" alt=""/></td><td width="16"><img src="//www.ibm.com/i/v14/icons/em.gif" height="16" width="16" vspace="3" alt="Email this page" /></td><td width="122"><p><a class="smallplainlink" href="javascript:void newWindow()"><b>E-mail this page</b></a></p></td></tr>'); //--> </script>
Email this page

E-mail this page

Sample code


Hey there! developerWorks is using Twitter

Follow us


Rate this page

Help us improve this content


Level: Intermediate

Jared Jurkiewicz (jaredj@us.ibm.com), Advisory Software Engineer, IBM
Stephanie L. Walter (swalt@us.ibm.com), Advisory Software Engineer, IBM

21 Oct 2008

Unit testing is an important part of quality software development, particularly in the agile and extreme programming development methodology. Traditionally, automated unit testing of Web 2.0 client-side user interfaces was difficult and often not attempted. However, Dojo provides a unit testing harness that lets you evaluate both JavaScript functionality and the visualization of the user interface. This results in a thoroughly tested user interface that will ultimately contain significantly fewer bugs. This article demonstrates the main features of the Dojo Objective Harness (DOH) and describes its superior capabilities compared with other test harnesses for Web 2.0 applications.

<script type="text/javascript"> // <![CDATA[ capture_referrer(); // ]]> </script>

The case for thorough unit testing

A unit test is usually written to test a piece of source code. Theoretically, this piece, or unit, should be the smallest testable portion of source code. Normally, a unit test is automated, but it does not have to be, and the result of the unit test indicates whether the code is behaving as designed.

It's common knowledge that the software developer is often crunched for time. There is extreme pressure to release products to the market as quickly as possible, so why spend even more time coding unit tests? The answer is that an adequate unit test suite not only produces higher quality code, but it also saves time in the end because you're likely to spend less time fixing bugs. And if you follow the agile development method, writing unit tests before attempting to write the source code will likely cause you to write less code. You will have thought through the design before simply coding away, which reduces the amount of code written to achieve the goal of the unit test.

 


Back to top


What is the Dojo Objective Harness?

There are a lot of supporters of unit testing, as can been seen in Extreme Programming, Agile, and so on. However, the widespread use of Asynchronous JavaScript + XML (Ajax) and Web 2.0 user interfaces has produced a need for client-side unit testing. The Dojo Objective Harness is the Web 2.0 UI developer's answer to JUnit. Unlike existing JavaScript unit test frameworks, such as JSUnit, the DOH not only provides a framework for automating JavaScript functions with or without the use of Dojo, it can also unit test the actual visualization of the user interface. This is because the DOH (what a great acronym) offers both command-line and browser-based interfaces to the testing framework.

 


Back to top


Browser and non-browser environment

As previously mentioned, the DOH provides a command-line interface as well as a browser-based interface. If the unit tests need to be fully automated and no visualization component is required, the command-line interface is a good choice because it can be kicked off by a build script and the results can be logged. Also, this interface provides a unit test environment very similar to JUnit. The DOH uses Rhino, an open-source JavaScript engine written in Java™ code, for its command-line interface. Because of this, references to the document, window, DOMParser, and XMLHttpRequest objects cannot be resolved. Another issue with Rhino is that it uses a different JavaScript interpreter than the popular browsers, so it is possible for a test to pass in one runtime and not another.

If the visual component of the unit tests and access to various JavaScript objects are required, the browser-based interface is your best bet. The caveat here is that unit tests using the browser are not 100% automated; you must launch the unit test in the desired browser and inspect the results. This is not totally surprising. Ensuring that a UI looks "good" is normally a subjective decision by a human. The browser test runner provides two ways to view the unit test results: visual results and unit test statistics. Figure 1 shows the test cases run on the left, with the visualization of the code execution on the right under the Test Page tab. (Click here to see a larger version of Figure 1.)


Figure 1. DOH unit test visualization
DOH unit test visualization

Figure 2 shows the unit test stats under the Log tab. (Click here to see a larger version of Figure 2.)


Figure 2. DOH unit test statistics
DOH unit test statistics


Back to top


Browser compatibility

As anyone developing client-side code for multiple browsers and versions knows, the ability to quickly detect differences in browser behavior through unit testing is important. Because the DOH test runner is just HTML and JavaScript, unit tests can be executed in any browser. This means that you can run unit tests in FireFox, Internet Explorer, and Safari (and different versions of each) and compare the results with one another. Not only can you ensure that basic JavaScript methods behave the same way across multiple platforms, you can also ensure that the visualization is the same, or at least acceptable, in the various platforms as well. We all know that a widget may look perfect on one browser, but is barely recognizable in another. Cross-browser bugs are often nasty and difficult to fix. Testing browser compatibility early, and in an automated way, ensures that cross-browser support problems are surfaced and fixed before the software gets to the market.

 


Back to top


Available test functions

Every test framework needs to supply you with a method to check the outcome of a unit test and the DOH is no exception. The DOH offers three assertion APIs for use in test verification, as shown in Listing 1.


Listing 1. Three assertion APIs

     doh.assertEqual(expectedResult, actualResult)
doh.assertFalse(testCondition)
doh.assertTrue(testCondition)

 

Additionally, the shorthand versions of these functions can be used. Listing 2 shows these versions.


Listing 2. Shorthand versions of the assertion APIs

     doh.is(expectedResult, actualResult)
doh.f(testCondition)
doh.t(testCondition)

 

When an assertion fails, an exception is thrown. If any type of exception is thrown in a unit test, the DOH declares the entire test as failed. This is important to know if you are expecting your test to throw an exception. In this case, you'll need to surround your code with a try catch block. When the unit tests are invoked, the DOH reports any errors that occurred and the specific test that has failed. The DOH also reports the total number of tests run, errors that occurred, and tests that failed.

When composing unit tests, it's best to keep the number of assertions to a minimum, as the DOH error reporting design can make it difficult to determine which assertion caused the failure. Although it's generally easier to determine which equals assertion caused the failure, true and false assertions are harder to find.

Sometimes errors occur in unit tests that aren't thrown by assertions. In these cases, either the unit test or the code to be tested is most likely incorrect. Luckily, the Firebug add-on for Firefox can also be used to debug fundamental code problems with unit tests.

 


Back to top


Asynchronous function testing

Wouldn't it be great to unit test the behavior of the asynchronous calls that a client-side application makes? The DOH can help. Testing the behavior of Ajax requests is one of the most valuable features of the DOH. Because its browser-based interface has access to the XMLHttpRequest object, the DOH can support asynchronous unit tests. To indicate that a test case is asynchronous, the test case alerts the harness by returning a doh.Deferred object. If the DOH is unaware that the test is asynchronous, after the code of the test is executed the DOH believes that the test is complete and no errors have occurred. Obviously, this leads to false positives and portions of your code left untested.

The test case itself must also be written with the understanding of an asynchronous context. When a doh.Deferred object is returned from a unit test, you must catch all errors from the asynch call and pass them to the object's errback method. If no exceptions occur, the object's callback method should be called with a parameter of true. This enables the DOH to report failed tests accurately.

To make writing asynchronous tests easier, the doh.Deferred object provides a getTestCallback function to implicitly handle exceptions that occur in the callback function of an asynchronous call. You just need to pass your test function to getTestCallback, which, in turn, contains the assertions that you want to execute. This relieves you of manually handling exceptions that occur during an asynchronous call. See Writing your own test suite for more details.

The DOH also allows you to specify a custom timeout in milliseconds that will fail the test if a response is not returned within the specified time. The default timeout value for asynchronous tests is 500 ms, or half a second, so many times it's a good idea to explicitly specify a longer timeout value so your test does not fail.

 


Back to top


Writing your own test suite

Writing your own test suite with the DOH can appear daunting at first, but it is actually not very difficult. The DOH framework is extremely flexible in how tests can be defined and loaded, so often the load flow can be modified to suit your particular structure. That said, the unit tests of Dojo almost all follow a common structure to make it simple for new module owners to pick up and run with it. It is recommended that you follow existing conventions until you are comfortable with how the DOH functions.

 


Back to top


The basic DOH test case structure

The test case structure is illustrated by using a demonstration module, demo.doh, that is placed as a peer module to the Dojo directory structure. The reason for the peer structure is that the DOH framework uses Dojo's module loader structure, and without doing a dojo.registerModulePath() to tell dojo where your source code is located, it assumes that your module directory is a peer directory to Dojo. While you can work with this by editing util/doh/runner.html to register your module paths, along with the import of doh.runner, ahead of time, it is simpler for beginning users to conform to the expectations of Dojo. Figure 3 shows the general directory structure that will be referred to throughout this section.


Figure 3. General directory structure
General directory structure

As Figure 3 shows, it is a good practice to have each dojo module contain unit tests for just that module. This lets the module developer run the unit tests separate from the overall project. That said, it does not mean that there cannot be a test suite module file that loads all the unit tests for all modules. Doing this will be covered in a later section of this article, after the basics of the structure are explained in detail.

 


Back to top


A demonstration set of test cases for the DOH

Before we get into the tests and how they work, it helps to understand what is being tested. In the case of demo.doh, it is a module that has a file containing helper functions and a simple DemoWidget. The reason for both is that they effectively illustrate how to test non-visual (JavaScript functions), as well as widgets used directly in html, just as how they are used in an application. These files implement trivial behaviors to make them easy to understand. Listing 3 shows the contents of the demoFunctions.js and Listing 4 shows the contents of the DemoWidget.js.


Listing 3. Contents of demoFunctions.js

dojo.provide("demo.doh.demoFunctions");

//This file contains a collection of helper functions that are not
//part of any defined dojo class.

demo.doh.demoFunctions.alwaysTrue = function() {
// summary:
// A simple demo helper function that always returns the boolean true when
// called.
// description:
// A simple demo helper function that always returns the boolean true when
// called.
return true; // boolean.
};

demo.doh.demoFunctions.alwaysFalse = function() {
// summary:
// A simple demo helper function that always returns the boolean false when
// called.
// description:
// A simple demo helper function that always returns the boolean false when
// called.
return false; // boolean.
};

demo.doh.demoFunctions.isTrue = function(/* anything */ thing) {
// summary:
// A simple demo helper function that returns true if the thing passed in is
// logically true.
// description:
// A simple demo helper function that returns true if he thing passed in is
// logically true.
// This means that for any defined objects, or Boolean values of true, it
// should return true,
// For undefined, null, 0, or false, it returns false.
// thing:
// Anything. Optional argument.
var type = typeof thing;
if (type === "undefined" || thing === null || thing === 0 || thing === false) {
return false; //boolean
}
return true; // Boolean
};

demo.doh.demoFunctions.asyncEcho = function(/* function */ callback,
/* string */ message){
// summary:
// A simple demo helper function that does an asynchronous echo
// of a message.
// description:
// A simple demo helper function that does an asynchronous echo
// of a message.
// The callback function is called and passed parameter 'message'
// two seconds
// after this helper is called.
// callback:
// The function to call after waiting two seconds. Takes one
// parameter,
// a string message.
// message:
// The message to pass to the callback function.
if (dojo.isFunction(callback)) {
var handle;
var caller = function() {
callback(message);
clearTimeout(handle);
handle = null;
};
handle = setTimeout(caller, 2000);
}
};



Listing 4. Contents of demo/doh/DemoWidget.js

dojo.provide("demo.doh.DemoWidget");
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");

dojo.declare("demo.doh.DemoWidget", [dijit._Widget, dijit._Templated],

//The template used to define the widget default HTML structure.
templateString: '<div dojoAttachPoint="textNode" style="width: 150px; ' +
' margin: auto; background-color: #98AFC7; font-weight: bold; color: ' +
'white; text-align: center;"></div>',

textNode: null, //Attach point to assign the content to.

value: 'Not Set', //Current text content.

startup: function() {
// summary:
// Overridden startup function to set the default value.
// description:
// Overridden startup function to set the default value.
this.setValue(this.value);
},

getValue: function() {
// summary:
// Simple function to get the text content under the textNode
// description:
// Simple function to get the text content under the textNode
return this.textNode.innerHTML;
},

setValue: function(value) {
// summary:
// Simple function to set the text content under the textNode
// description:
// Simple function to set the text content under the textNode
this.textNode.innerHTML = value;
this.value = value;
}
});



Back to top


Testing stand-alone functions, both synchronous and asynchronous, in the DOH

As you can see in Listings 3 and 4, we have implemented a simple widget and small set of stand-alone functions. Now that they have been defined, it would be great to implement unit tests that exercise the functions and the widgets to confirm they behave as expected. With other JavaScript Unit Test frameworks, the synchronous functions would be easily testable, but the asynchronous function demo.doh.demoFunctions.asyncEcho and the widget would not. So, enter the DOH and its facility for handling widget testing in browsers as well as synchronous function testing.

The simplest place to begin is to test stand-alone functions. Writing stand-alone function test cases is as simple as defining a JavaScript array. The array should contain test functions, test fixtures, or a mix of both. The complexity of what you're testing determines which one you should use. In most cases, simple test functions are more than adequate for testing code. It is only when you need to alter timeouts, perform setup operations, or tear down data after a test that you would need to construct a test fixture. After the array of functions have been defined, to register them with the DOH is a matter of calling the tests.register with two parameters, the name you want to assign to the collection of tests, and the array of the tests. Listing 5 is the code listing for a small set of tests for the demoFunctions.js stand-alone functions.


Listing 5. Contents of demo/doh/tests/functions/demoFunctions.js

dojo.provide("demo.doh.tests.functions.demoFunctions");

//Import in the code being tested.
dojo.require("demo.doh.demoFunctions");

doh.register("demo.doh.tests.functions.demoFunctions", [
function test_alwaysTrue(){
// summary:
// A simple test of the alwaysTrue function
// description:
// A simple test of the alwaysTrue function
doh.assertTrue(demo.doh.demoFunctions.alwaysTrue());
},
function test_alwaysFalse(){
// summary:
// A simple test of the alwaysFalse function
// description:
// A simple test of the alwaysFalse function
doh.assertTrue(!demo.doh.demoFunctions.alwaysFalse());
},
function test_isTrue(){
// summary:
// A simple test of the isTrue function
// description:
// A simple test of the isTrue function with multiple permutations of
// calling it.
doh.assertTrue(demo.doh.demoFunctions.isTrue(true));
doh.assertTrue(!demo.doh.demoFunctions.isTrue(false));
doh.assertTrue(demo.doh.demoFunctions.isTrue({}));
doh.assertTrue(!demo.doh.demoFunctions.isTrue());
doh.assertTrue(!demo.doh.demoFunctions.isTrue(null));
doh.assertTrue(!demo.doh.demoFunctions.isTrue(0));
},
{
//This is a full test fixture instead of a stand-alone test function.
//Therefore, it allows over-riding of the timeout period for a deferred test.
//You can also define setup and teardown function
//for complex tests, but they are unnecessary here.
name: "test_asyncEcho",
timeout: 5000, // 5 seconds.
runTest: function() {
// summary:
// A simple async test of the asyncEcho function.
// description:
// A simple async test of the asyncEcho function.
var deferred = new doh.Deferred();
var message = "Success";
function callback(string){
try {
doh.assertEqual(message, string);
deferred.callback(true);
} catch (e) {
deferred.errback(e);
}
}
demo.doh.demoFunctions.asyncEcho(callback, message);
return deferred; //Return the deferred. DOH will
//wait on this object for one of the callbacks to
//be called, or for the timeout to expire.
}
}
]);

 

As Listing 5 shows, defining a basic set of tests does not require a lot of code per test, even if it requires a test fixture to execute it due to needing to alter the default timeout. The tests also show one of the other best practices for writing unit tests; keep the tests as simple and small as possible. The reason to only have a few asserts per test is that it makes it quicker to isolate the failure in the test from the error the DOH reports. Too many asserts can make it difficult to determine exactly which assert caused the error.

The other point of interest to note with the tests is how the async tests usually need to be written. Because the callback runs later, it cannot be easily try/catch caught by the DOH when there is a failure, like it does for a synchronous test. Instead, the unit test must take this into account. With the asyncEcho test, it wraps the asserts in a try/catch, and any errors are passed back to the DOH through the deferred.errback(error) call. If wrapping was not done, the test would still fail on an error, but all the DOH would report is the test timed out. This is because the error thrown from the failed assert prevented the deferred.callback() from being executed. So, the test, according to the DOH, never reported completion, and therefore, gets timed out. In other words, the only way the DOH knows an async test passed or failed is if an operation is invoked on the Deferred.

 


Back to top


Testing widgets in the DOH

As the previous section shows, testing simple stand-alone functions is easy to do. You create an array of functions or test fixtures, register them, and the DOH will execute them when loaded. That's great, but stand-alone functions and non-visual code isn't what JavaScript is all about; it's about manipulating the browser DOM to provide a more interactive look and feel. So, the next question to explore is how do you test widgets?

The great news is that the DOH provides a good framework and method for registering tests that require a Web browser to load an HTML file that instantiates the widgets to be tested. Effectively, what the DOH does is create a bridge between an instance of the DOH running in an HTML file in an iframe and the instance of the DOH running its UI and stand-alone tests. Something to remember here is that unlike the stand-alone function tests, widget tests cannot generally be run headless through a JavaScript interpreter like Rhino.

So, how do you define widget tests? Well, you define an HTML file that instantiates the DOH, instantiates widgets, then defines the test functions to execute. Listing 6 shows a code listing of an HTML file that makes use of the DOH to test demo.doh.DemoWidget.


Listing 6. Contents of demo/doh/tests/widgets/DemoWidget.html

<html>
<head>
<title>DemoWidget Browser Tests</title>
<script type="text/javascript" src="../../../../dojo/dojo.js"
djConfig="isDebug: true, parseOnLoad: true"></script>
<script type="text/javascript">
dojo.provide("demo.doh.tests.widgets.DemoWidgetHTML");
dojo.require("dojo.parser");
dojo.require("doh.runner");
dojo.require("demo.doh.DemoWidget");

dojo.addOnLoad(function(){
doh.register("demo.doh.tests.widgets.DemoWidget", [
function test_DemoWidget_getValue(){
// summary:
// Simple test of the Widget getValue() call.
doh.assertEqual("default", dijit.byId("demoWidget").getValue());
},
function test_DemoWidget_setValue(){
// summary:
// Simple test of the Widget setValue() call.
var demoWidget = dijit.byId("demoWidget");
demoWidget.setValue("Changed Value");
doh.assertEqual("Changed Value", demoWidget.getValue());
}
]);
//Execute D.O.H. in this remote file.
doh.run();
});
</script>
</head>
<body>
<!-- Define an instance of the widget to test. -->
<div id="demoWidget" dojoType="demo.doh.DemoWidget" value="default"></div>
</body>
</html>


 

So, as Listing 6 shows, it is a stand-alone file that runs the DOH. That's great, but it doesn't display the DOH's UI, so it's difficult to tell if tests pass or not. It would be great if the DOH provided a mechanism that could still run this HTML file and still display the UI. Well, good news, it can. The DOH has a another test registration function called doh.registerUrl(). This function lets you point the DOH runner.html UI at a separate HTML file. What it will then do is load that HTML file into a frame, connect the DOH instance created by that HTML file with the UI's DOH instance, and then the UI can also display failures and successes from the HTML page! Listing 7 shows the code for the module file that registers a URL as a source of tests and results.


Listing 7. Contents of demo/doh/tests/widgets/DemoWidget.js

dojo.provide("demo.doh.tests.widgets.DemoWidget");

if(dojo.isBrowser){
//Define the HTML file/module URL to import as a 'remote' test.
doh.registerUrl("demo.doh.tests.widgets.DemoWidget",
dojo.moduleUrl("demo",
“doh/tests/widgets/DemoWidget.html"));
}



Back to top


Bringing it all together: Combining the test definitions into a single DOH test suite

You have now seen how to write individual test files. As demonstrated, writing single tests is not complicated. So, the question that remains is how do you take these test definitions, load them into the DOH's UI, and execute them. This is also not difficult. You write an HTML file that redirects to the runner.html of the DOH. As part of the redirect, you pass a query parameter that defines what JavaScript module file to load. This single module file, usually called module.js, uses dojo.require() to load each of your test files. When the dojo.require() brings the files in, they register the tests. When all test files have been loaded by the DOH the framework automatically executes the tests. Listing 8 is the redirection file. Listing 9 is the module.js file that brings in all your test files.


Listing 8. Contents of demo/doh/tests/runTests.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>demo.doh Unit Test Runner</title>
<meta http-equiv="REFRESH"
content="0;url=../../../util/doh/runner.html?testModule=demo.doh.tests.module">
</head>
<body>
Redirecting to D.O.H runner.
</body>
</html>



Listing 9. Contents of demo/doh/tests/module.js

dojo.provide("demo.doh.tests.module");
//This file loads in all the test definitions.

try{
//Load in the demoFunctions module test.
dojo.require("demo.doh.tests.functions.demoFunctions");
//Load in the widget tests.
dojo.require("demo.doh.tests.widgets.DemoWidget");
}catch(e){
doh.debug(e);
}



Back to top


In conclusion

While the DOH can be daunting for the novice user, it is a flexible and powerful unit testing framework. It modularizes tests into separately loadable files, provides functions to associate tests as groups, provides a series of test APIs to assert conditions about the code being executed, and even provides a framework for handling asynchronous tests and browser tests for widgets through URL registration and iframe page loading.

By looking at the DOH piece by piece, the complexity disappears. Writing simple test cases is quick and easy, and combining those test cases into a suite is nothing more than writing a single JavaScript file that dojo.require()'s in each separate set of tests. This module file becomes your test suite entry point. The DOH also provides a powerful UI that shows success, failures, and even what errors were thrown. To make use of it, all that has to occur is that the runner.html is loaded with a query parameter defining which file to load that will register tests.

Lastly, the DOH is not limited to browser environments. The basic DOH loader and framework can be used in headless JavaScript environments such as SpiderMonkey and Rhino. The DOH is truly one of the most complete and effective frameworks for testing JavaScript code.



Back to top


Download

DescriptionNameSizeDownload method
Source codedemo.doh.zip5KBHTTP
Information about download methods


Resources


About the authors

Photo of Jared Jurkiewicz

Jared Jurkiewicz is an advisory software engineer in the WebSphere® family of products. He has held many roles in the WebSphere organization, from being a UNIX® operating systems expert, to being the lead on handling initial support of new operating systems and hardware platforms. His current assignment is as the release architect for WebSphere FeaturePack for Web 2.0, and he is also a contributor and committer to the Dojo Toolkit.

 

Photo of Stephanie Walter

Stephanie Walter is an advisory software engineer on the Tivoli Service Availability and Performance Management architecture team. She previously led the development of WebSphere Business Monitor Dashboards and has worked extensively with Dojo and Web 2.0 technologies.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值