您的位置:首页 > 移动开发 > Objective-C

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

2009-01-22 11:41 471 查看

级别: 中级

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 引擎。正因如此,对
document
window
DOMParser
XMLHttpRequest
对象的引用无法被解析。Rhino 的另一个问题是它使用了一个与一般浏览器不同的 JavaScript 解释程序,这使得测试有可能在一个运行时内通过,而在另一个运行时内则不能。

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

图 1. DOH 单元测试可视化



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

图 2. 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
<tr
valign="top"><td width="8"><img alt="" height="1" width="8"
src="//www.ibm.com/i/c.gif"/></td><td width="16"><img alt="" width="16"
height="16" src="//www.ibm.com/i/c.gif"/></td><td class="small"
width="122"><p><span class="ast">Document options requiring JavaScript
are not displayed</span></p></td></tr>
<!--
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>');
//-->

<!-- 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>');
//-->



Print this page



PDF - Fits A4 and Letter
167KB



Get Adobe® Reader®



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.

// <![CDATA[
capture_referrer();
// ]]>

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



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






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



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

The Dojo toolkit Web site provides documentation on Dojo and the Dojo Objective Harness.

Dustin Machi wrote an insightful blog
entry on unit testing with the DOH.

Learn more about unit testing.

Rhino
is the Java-based JavaScript interpreter used by the DOH.

Agile software development and extreme programming both promote writing unit test cases before developing the source code.

More information on other Ajax
technologies (including Dojo) can be found in the developerWorks Ajax resource center.

You can also get a complete reference of
the Dojo API.

About the authors





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.





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.

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: