天天看點

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試

用 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 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

什麼是 Dojo Objective Harness?

單元測試有衆多的支援者,正如在極限程式設計以及靈活程式設計中看到的那樣。Asynchronous JavaScript + XML (Ajax) 及 Web 2.0 使用者界面的廣泛使用催生了對用戶端單元測試的需求。Dojo Objective Harness 是 Web 2.0 UI 開發人員用于 JUnit 的工具。與已有的 JavaScript 單元測試架構(比如 JSUnit)不同,DOH 不僅能夠實作在使用或不使用 Dojo 的情況下自動處理 JavaScript 函數,它還可以對使用者界面的可視性 進行單元測試。這是因為 DOH(多好的縮寫名)既提供了指令行界面,也提供了基于浏覽器的界面來測試架構。

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

浏覽器和非浏覽器環境

前面提到過,DOH 既提供指令行界面,又提供了基于浏覽器的界面。如果單元測試需要完全自動化,并且不需要可視元件,那麼指令行界面是個不錯的選擇,這是因為它可通過一個構 建腳本啟動,且其結果可被記錄。此外,這個界面還提供了一個與 JUnit 非常相似的單元測試環境。DOH 為其指令行界面還使用了 Rhino,一個用 Java™ 代碼編寫的開源 JavaScript 引擎。正因如此,對

document

window

DOMParser

XMLHttpRequest

對象的引用無法被解析。Rhino 的另一個問題是它使用了一個與一般浏覽器不同的 JavaScript 解釋程式,這使得測試有可能在一個運作時内通過,而在另一個運作時内則不能。

如果單元測試需要可視元件和通路各種 JavaScript 對象,那麼基于浏覽器的界面将是最佳選擇。需要提醒您的是使用浏覽器的單元測試并不是 100% 自動的;您必須在自己衷愛的浏覽器中啟動單元測試并要檢查其結果。其實這并不意外。一個 UI 外觀的好壞通常是人的主觀判斷。浏覽器測試的運作程式提供了兩個途徑來顯示測試結果:一個是可視化結果,另一個是單元測試統計資料。圖 1 在左側顯示了運作的測試用例,而在右側 Test Page 頁籤下則可視化顯示了代碼執行(單擊 這裡 可以看到圖 1 的放大圖)。

圖 1. DOH 單元測試可視化

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試

圖 2 顯示了在 Log 頁籤下的單元測試統計資料(單擊 這裡 可以看到圖 2 的放大圖)。

圖 2. DOH 單元測試統計資料

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

浏覽器的相容性

針 對多種浏覽器和版本開發過用戶端代碼的人都知道,要能通過單元測試快速檢測到浏覽器行為的差别,這一點非常重要。因為 DOH 測試運作程式是 HTML 和 JavaScript,是以單元測試可以在任何浏覽器中執行。這就意味着您可以在 FireFox、Internet Explorer 和 Safari (及它們的不同版本)中運作測試并比較各自的結果。您不僅可以確定基本 JavaScript 方法在各種平台中都有相同的表現,而且還可以確定可視化在各種平台中也是相同的,或至少差不多。我們都知道一個小部件在一個浏覽器可能表現良好,但在其他 浏覽器中就不一定了。跨浏覽器的 bug 通常很令人讨厭且很難被修複。若能提前自動地測試浏覽器的相容性,就可以在軟體投入市場前及時發現和修複跨浏覽器支援的問題。

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

可用的測試函數

每個測試架構都要為開發人員提供檢查單元測試結果的方法,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 插件 可被用來調試單元測試中的基礎代碼問題。

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

異步函數測試

若能對用戶端應用程式發出的異步調用的行為進行單元測試,豈不是很棒?DOH 可以幫得上這個忙。測試 Ajax 請求的行為是 DOH 最有價值的特點之一。因為借助其基于浏覽器的界面可以通路

XMLHttpRequest

對象,是以 DOH 可以支援異步單元測試。要訓示一個測試用例是異步的,此測試用例需要通過傳回一個

doh.Deferred

對象來提示 DOH。如果 DOH 不知道這個測試是異步的,那麼在此測試的代碼執行之後,DOH 就會認為此測試已完成,沒有錯誤發生。顯然,這将導緻測試成功的假象,而且還會使得部分代碼得不到測試。

必須要在了解異步上下文的基礎上對這個測試示例本身進行編寫。當從單元測試中傳回一個

doh.Deferred

對象時,必須捕獲異步調用中産生的所有錯誤資訊并把它們傳遞給對象的

errback

方法。如果沒有異常發生,就應該用一個真值參數調用這個對象的

callback

方法。這能使 DOH 準确地報告失敗的測試。

為了使編寫異步測試變得簡單,

doh.Deferred

對象提供了一個

getTestCallback

函數來隐式地處理在異步調用的回調函數中發生的異常。您隻需将測試函數傳遞給

getTestCallback

,而它反過來包含了所想執行的斷言。這能讓您不用再手工處理異步調用過程中發生的異常。更多資訊,請參見 編寫自已的測試套件 一節。

DOH 還允許以毫秒為機關設定逾時值,一旦響應沒有在指定的時間内傳回,測試就會失敗。異步測試的預設逾時值是 500ms,也就是半秒,是以,很多時候,最好是顯式地指定一個更長的逾時值,這樣一來,測試就不會失敗。

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

編寫自已的測試套件

用 DOH 編寫自已的測試套件初看上去很複雜,但實際上它并不難。DOH 架構對如何定義和加載測試的要求很靈活,通常可以修改加載流程以适合您具體的結構。Dojo 的單元測試幾乎都遵循通用的結構以使新子產品所有者便于上手和使用。建議您在熟練掌握 DOH 工作原理之前,最好遵循現有的約定。

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

DOH 測試用例的基本結構

通過一個示範子產品 demo.doh,可以說明測試用例結構,該子產品作為一個 Dojo 目錄結構的對等子產品。之是以采用對等結構是因為 DOH 架構使用 Dojo 的子產品加載程式結構,并且沒有用

dojo.registerModulePath()

告知 Dojo 源代碼在什麼位置,它假定子產品目錄是 Dojo 的對等目錄。然而,這可以按如下方式得到解決:編輯 util/doh/runner.html 來注冊子產品路徑,若再能提前導入 doh.runner,将會使初級使用者很容易就能遵循 Dojo 的約定。圖 3 顯示了這個通用的目錄結構,該結構會在本節中多次提到。

圖 3. 通用的目錄結構

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試

如圖 3 所示,讓每個 Dojo 子產品都包含隻針對該子產品的單元測試是個很好的做法。這使子產品開發者能夠在獨立于整個項目的情況下運作單元測試。但這不意味着不允許任何能夠加載所有子產品的 全部單元測試的測試套件子產品檔案的存在。有關内容會在詳細介紹完此結構的基礎知識後,在本文後面的章節給出。

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

一組 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;
     }
});
      
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

在 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 得知異步測試是通過還是失敗的惟一途徑就是:操作是否在延遲操作上被調用了。

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

在 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"));
}

      
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

把它們放在一起:将測試定義合并到單個 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);
}
      
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
回頁首

結束語

盡管 DOH 對一個新手來說有些複雜,但它的确是一個靈活且強大的單元測試架構。它将測試子產品化為可單獨加載的檔案,并提供函數以将測試合并成組,此外還提供了一系列 測試 API 來斷言執行代碼的條件,甚至還通過 URL 注冊和 iframe 頁面加載提供了處理異步測試以及浏覽器小部件測試的架構。

通過對 DOH 進行仔細分析,我們發現它并不複雜。編寫一個簡單的測試用例很快也很容易,把這些測試示例合并成一個套件也隻需編寫一個 JavaScript 檔案即可,其中

dojo.require()

要包括在每組單獨的測試中。此子產品檔案就是測試套件的入口點。DOH 還提供了一個強大的 UI ,可用來顯示成功或失敗甚至抛出的錯誤。要想利用它,隻需要用一個定義所要加載檔案的查詢參數加載 runner.html,此檔案将用來注冊測試。

最後,DOH 不隻限于浏覽器環境。基礎 DOH 加載程式和架構均能用于 JavaScript 環境中,例如 SpiderMonkey 和 Rhino。DOH 的确是測試 JavaScript 代碼的最完整和最有效的架構之一。

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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> <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()" target="_blank" rel="external nofollow" >Print this page</a></b></p></td></tr>'); //--> </script>
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
Print this page
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試

PDF - Fits A4 and Letter

167KB

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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()" target="_blank" rel="external nofollow" ><b>E-mail this page</b></a></p></td></tr>'); //--> </script>
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
E-mail this page
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
Sample code
Hey there! developerWorks is using Twitter
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
Follow us
Rate this page
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
Help us improve this content

Level: Intermediate

Jared Jurkiewicz ([email protected]), Advisory Software Engineer, IBM

Stephanie L. Walter ([email protected]), 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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試

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

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試

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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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;
     }
});
      
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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"));
}

      
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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);
}
      
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
Back to top

Download

Description Name Size Download method
Source code demo.doh.zip 5KB HTTP
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
Information about download methods
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試

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

用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
用 Dojo Objective Harness 對 Web 2.0 應用程式進行單元測試
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.

繼續閱讀