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

2009-01-14 作者:Jared Jurkiewicz,Stephanie L. Walter 来源:IBM

 
本文内容包括:
单元测试是保证软件开发质量的一个重要部分,对于敏捷和极限编程开发方法尤其如此。通常,对 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 单元测试可视化
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 代码的最完整和最有效的框架之一。

下载

描述 名字 大小 下载方法
源代码
demo.doh.zip
5KB

参考资料


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织