本文章是由機器翻譯。

測試運行

使用 jQuery 進行 Web 應用程式 UI 測試

James McCaffrey

下載代碼示例

jQuery 庫是一個開放的 JavaScript 函數源集合。儘管在創建 jQuery 時考慮了 Web 開發,但庫有幾個特性使其非常適合於輕型 Web 應用程式 UI 測試自動化。在本月的專欄中,我將為您演示如何做到這一點。

要想瞭解我將講述的內容,最好是看一下圖 1 中的螢幕擷取畫面,圖中演示了實際運行的使用 jQuery 實現的 UI 測試自動化。測試工具由 Internet Explorer 承載,並包含名為 UITestHarness.html 的 HTML 頁面。

工具頁面實際上就是一個帶有兩個 HTML frame 元素的容器。右側的框架保留了待測試的 Web 應用程式,本例中將介紹一個名為 MiniCalc 的 ASP.NET 計算器應用程式,該應用程式很簡單但具有代表性。左側的框架保留了一個名為 TestScenario001.html 的 HTML 頁面,該頁面包含一個用於顯示進度消息的 TextArea 元素、一個用於手動啟動自動化的 Button 元素以及一些基於 jQuery 的 JavaScript 函數,這些函數用於操作待測試的 Web 應用程式並檢查該應用程式的結果狀態,以確定結果是通過還是失敗。

圖 1 使用 jQuery 進行 UI 測試自動化

jQuery 庫也非常適於 HTTP 請求/回應測試,而我在 2010 年 1 月的“測試運行”專欄 (msdn.microsoft.com/magazine/ee335793) 中使用了 jQuery 來處理請求/回應測試。

本文假定您已基本熟悉 ASP.NET 技術和中級 JavaScript 程式設計技能,並假定您沒有使用 jQuery 庫的任何經驗。然而,即使您是剛剛接觸 ASP.NET 和測試自動化,本月專欄所介紹的內容對您來說應該也不難理解。

在下麵幾節中,我將首先介紹 MiniCalc 應用程式,從而使您準確理解待測試的應用程式的實現與 UI 測試自動化的關係。接下來,我將向您介紹創建基於 jQuery 的輕型 UI 測試自動化的詳細資訊,如圖 1 所示。最後我將說明如何擴展我所介紹的技術來滿足您自己的需求,並將討論 jQuery UI 測試自動化與其他方法相比的優勢和劣勢。我相信此處介紹的技術相當有趣,並且可以成為您的測試、開發和管理工具集的有用補充。

待測試的應用程式

我們來看看作為基於 jQuery 的 UI 測試自動化的目標的 MiniCalc ASP.NET Web 應用程式的代碼。

我使用 Visual Studio 2008 創建了 MiniCalc 應用程式。在啟動 Visual Studio 後,按一下“檔”|“新建”|“網站”。為了避免使用 ASP.NET 代碼隱藏機制並將 Web 應用程式的所有代碼保存在一個檔中,我選擇了“空網站”選項。接下來,從“位置”欄位下拉清單中選擇“HTTP 模式”選項(而非“檔模式”),並將位置指定為:

http://localhost/TestWithJQuery/MiniCalc

我決定對 MiniCalc 應用程式邏輯使用 C#。 此處介紹的測試自動化技術可與以 C# 和 Visual Basic 編寫的 ASP.NET Web 應用程式以及通過傳統的 ASP、CGI、PHP、JSP、Ruby 等技術創建的 Web 應用程式一起使用。

在“新建網站”對話方塊上按一下“確定”,以配置 IIS 並生成 Web 應用程式的結構。 接下來,轉到解決方案資源管理器視窗,按右鍵 MiniCalc 專案名稱,並從上下文功能表中選擇“添加新項”。 然後,從已安裝的範本清單中選擇“Web 表單”,並接受 Default.aspx 檔案名。 我清除了“將代碼放在單獨的檔中”選項,然後按一下“添加”按鈕。

接下來,在解決方案資源管理器中按兩下 Default.aspx 檔案名,以將範本生成的代碼載入到文字編輯器。 我刪除了所有範本代碼並替換為圖 2 中所示的代碼。

圖 2 待測試的 MiniCalc Web 應用程式的源

<%@ Page Language="C#" %>
<script runat="server">
  static Random rand = null;
  private void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack) 
      rand = new Random(0);
  }
  private void Button1_Click(object sender, System.EventArgs e)
  {
    int randDelay = rand.Next(1, 6); // [1-5]
    System.Threading.Thread.Sleep(randDelay * 1000);
    int x = int.Parse(TextBox1.Text);
    int y = int.Parse(TextBox2.Text);
    if (RadioButton1.Checked)
      TextBox3.Text = (x + y).ToString("F4");
    else if (RadioButton2.Checked)
      TextBox3.Text = (x * y).ToString("F4");
  }
</script>
<html>
  (client-side JavaScript and UI elements here)
</html>

為確保應用程式碼盡可能小並且易於理解,我省略了常規錯誤檢查。 可以從 code.msdn.microsoft.com/mag201012TestRun 處獲取 MiniCalc 應用程式的完整原始程式碼和測試工具。

若要為 Web 應用程式編寫測試自動化,您通常必須知道各使用者控制項的 ID。 如圖 2 所示,我使用 TextBox1 和 TextBox2 來保留兩個使用者整數輸入值(RadioButton1 和 RadioButton2)以選擇加法或乘法,並使用 TextBox3 來保留算術計算結果。

使用者按一下 Button1 控制項時,MiniCalc 應用程式首先會進入 1 至 5 秒的隨機延遲,以類比某種伺服器端處理,然後計算並顯示兩個使用者輸入值的總和或乘積。

接下來,我決定使用 AJAX 技術讓 MiniCalc 應用程式實現非同步。 要完成此任務,我需要為應用程式創建一個 web.config 檔,而無需從頭手動創建一個 web.config 檔。我按下 F5 鍵以指示 Visual Studio 通過調試器生成並運行應用程式。 當 Visual Studio 提示我允許添加 web.config 檔時,我按一下了“確定”。 接下來,我向 MiniCalc 應用程式中添加了一個 ScriptManager 伺服器端控制項,以啟用 AJAX:

    <asp:ScriptManager ID="sm1" runat="server" EnablePartialRendering="true" />

然後,我添加了非同步更新與 Button1 按一下事件結合的 TextBox3 結果元素所必需的標記:

    <asp:UpdatePanel ID="up1" runat="server">
    <ContentTemplate>
    <p><asp:TextBox id="TextBox3" width="120"  runat="server" />
    </ContentTemplate>
    <Triggers>
    <asp:AsyncPostBackTrigger ControlID="Button1" EventName="Click" />
    </Triggers>
    </asp:UpdatePanel>

如果仔細檢查圖 1,您將會發現,為了強調 MiniCalc 是 AJAX 應用程式的事實,我在 UI 中放置了一個用戶端頁面生命計數器。 當對 MiniCalc 的非同步請求返回時,只會更新 TextBox3,而不會重置頁面生命計數器。 將 pageLife 文字方塊定義為:

<input type="text" id="pageLife" size="1"/>

相關的用戶端 JavaScript 為:

<script language="javascript">
  var count = 0;
  function updatePageLife() {
    ++count;
    var tb = document.getElementById("pageLife");
    tb.value = parseInt(count);
    window.setTimeout(updatePageLife, 1000);
  }
</script>

計數器由應用程式 onload 事件啟動:

<body bgColor="#ccffff" onload="updatePageLife();">

使用 jQuery 進行 Web 應用程式 UI 測試

現在,您已瞭解了待測試的 Web 應用程式,讓我們直接開始探討 UI 測試自動化代碼。 主要的測試工具只是帶有兩個 frame 元素的普通 HTML 頁面:

    <html>
    <!-- UITestHarness.html -->
    <head>
      <title>Test Harness for MiniCalc AJAX Web App</title>
    </head>
      <frameset cols="45%,*" onload="leftFrame.appLoaded=true">
        <frame src="http://localhost/TestWithJQuery/TestScenario001.html"
           name="leftFrame" >
        <frame src="http://localhost/TestWithJQuery/MiniCalc/Default.aspx"
           name="rightFrame">
      </frameset>
    </html>

名為 rightFrame 的框架按原樣承載待測試的 Web 應用程式,該應用程式從未進行過任何修改或測試檢測。 名為 leftFrame 的框架承載名為 TestScenario001.html 的 HTML 頁面,該頁面包含所有 jQuery 測試自動化代碼。 請注意,當觸發 frameset 元素 onload 事件時,leftFrame 頁中名為 appLoaded 的變數將設置為 true。 此變數將用於確保在待測試的 Web 應用程式完全載入到測試工具之後才開始進行測試自動化。 測試方案代碼的結構如圖 3 所示。

圖 3 UI 測試自動化頁的結構

    <html>
    <!-- TestScenario001.html -->
    <head>
      <script src='http://localhost/TestWithJQuery/jquery-1.3.2.js'></script>
      <script type="text/javascript">
        $(document).ready(function() {
          logRemark("jQuery Library found and harness DOM is ready\n");
        } );
      
        var testScenarioID = "Test Scenario 001";
        var maxTries = 20;
        var numTries;
        var polling = 500; // milliseconds
        var appLoaded = false;
        var started = false;
        
        function launch() {
          if (!started)
            runTest();
        }
        
        function waitUntilAppLoaded() {
          // Code
        }
        
        function runTest() {
          // Start automation
        }
        
        function step1() {
          // Manipulate state
        }
        function clickCalculate() {
          // Click the Calculate button
        }
        function checkControl(controlID, controlVal) {
          // Determine if control has specified value
        }
        
        function step2() {
          // Manipulate state
        }
        
        function callAndWait(action, checkControlFunc, controlID, controlVal,
          callbackFunc, pollTime) {
          // The heart of the automation
        }
        function doWait(checkControlFunc, controlID, controlVal, 
          callbackFunc, pollTime) {
          // Wait until Web app responds
        }
        
        function finish() {
          // Determine pass/fail result
        }
           
        function logRemark(comment) {
          // Utility logging function
        }
      </script>
    </head>
    <body bgcolor="#F5DEB3">
      <h3>This is the UI test scenario with jQuery script page</h3>
      <p>Actions:</p><p><textarea id="comments" rows="22" cols="34">
      </textarea></p>
      <input type="button" value="Run Test" onclick="runTest();" /> 
    </body>
    </html>

測試腳本首先會引用 jQuery 庫:

<script src='http://localhost/TestWithJQuery/jquery-1.3.2.js'>

這裡所指向的是已從 jQuery 專案網站 (jquery.com) 中下載並複製到 MiniCalc 應用程式根目錄的 jQuery 庫的本機複本。 我使用了 jQuery 版本 1.3.2。 我們一直在不斷地開發該庫,所以您在閱讀本文的時候可能存在較新的版本。 有關在您的代碼中引用 jQuery 庫的詳細資訊,請參閱“獲取 jQuery 庫”。

獲取 jQuery 庫

對於您的應用程式所使用的 jQuery 庫的位置,您可以有多個選擇。 如前文所述,您可從 jquery.com 中下載最新版本並從您的本地檔案系統中使用它。 jQuery 網站提供了開發(未壓縮)和生產(已縮小且已刪除空格,以佔用較少的空間)下載。 只需選擇您想要的包,並將 .js 檔保存到您的專案目錄即可。

如果您的應用程式主機有活動的 Internet 連接,則還有更簡單的方法,那就是指向線上內容傳送網路 (CDN) 提供的 jQuery 的最新版本。 您有大量資源(包括您自己的託管版本)可以使用,但有兩個 CDN 是高度可用的,即 Microsoft AJAX 內容傳送網路 (asp.net/ajaxlibrary/cdn.ashx) 和 Google Libraries API (code.google.com/apis/libraries)。

例如,您可使用 Microsoft Ajax CDN 中帶有以下腳本標記的縮小版 jQuery:

<script 
  src="http://ajax.microsoft.com/ajax/jquery/jquery-1.3.2.min.js" 
  type="text/javascript">
</script>

Scott Guthrie 發表過一篇很有用的、關於對 jQuery 和 ASP.NET AJAX 使用 Microsoft Ajax CDN 的博客文章,網址為 tinyurl.com/q7rf4w

通常,將 jQuery 用於測試自動化時,使用測試工具中的庫的本地未打包副本比使用遠端或已打包副本更可靠。 但對於生產應用程式,您將希望使用一個可靠的託管庫。

接下來,我使用一個標準的 jQuery 慣例來確定自動化是否具有訪問 jQuery 庫的許可權:

$(document).ready(function() {
  logRemark("jQuery Library found and harness DOM is ready\n");
} );

當包含文檔 DOM 完全載入到測試主機記憶體,並且所有的 DOM 元素都可用後就會立即觸發 jQuery ready 函數。 如果 jQuery 庫無法訪問(指定了不正確的庫路徑時通常會發生這種情況),則會引發“缺少物件”錯誤。

ready 函數會將匿名函數當作其單個參數接受。 匿名函數會頻繁地用於對 jQuery 和 JavaScript 的測試自動化中。 您可將匿名函數當作使用函數關鍵字動態定義的函數。

下麵是一個名為 logRemark 的函數的示例:

function logRemark(comment) {
  var currComment = $("#comments").val();
  var newComment = currComment + "\n" + comment;
  $("#comments").val(newComment);
}

這種情況下,我定義了一個函數,該函數只調用一個名為 logRemark 的程式定義的日誌記錄函數來對 jQuery 可用的測試工具顯示消息。 同時,我還使用了內部 JavaScript 警告函數。

首先,我使用 jQuery 選擇器和連結語法獲取 ID 為“comments”的文本區中的當前文本。記號 $ 是 jQuery 元類的快捷別名。 # 語法用於按照 ID 選擇 HTML 元素,而 val 函數可充當值 setter 和 getter(物件導向的程式設計術語中的屬性)。 我在現有注釋文本中附加了一個 comment 參數和換行字元,然後使用 jQuery 語法更新 TextArea 元素。

接下來,我設置了一些測試自動化全域變數:

var testScenarioID = "Test Scenario 001";
var maxTries = 20;
var numTries;
var polling = 500;
var appLoaded = false;
var started = false;

因為我的自動化處理的是非同步應用程式,所以不使用任意時間延遲。 相反,我使用一系列短暫(由變數輪詢定義)延遲,再由變數 numTries 進行反復檢查,以查看某些 HTML 元素的值是否滿足 Boolean 條件,檢查次數最多達到變數 maxTries 的最大嘗試次數。 在本測試方案中,我在總共十秒的時間內最多對 20 次嘗試使用了延遲,每次嘗試的延遲時間為 500 毫秒。 appLoaded 變數用於確定待測試的 Web 應用程式完全載入到測試工具的時間。 started 變數用於協調測試工具的執行。

若要手動啟動自動化,您可按一下“運行測試”按鈕:

<input type="button" value="Run Test" onclick="runTest();" />

圖 3 中所示的 launch 函數用於完全測試自動化,稍後我會進行解釋。 runTest 函數將充當測試自動化的主要協調函數:

function runTest() {
  waitUntilAppLoaded();
  started = true;
  try {
    logRemark(testScenarioID);
    logRemark("Testing 3 + 5 = 8.0000\n");
    step1();
  }
  catch(ex) {
    logRemark("Fatal error: " + ex);
  }
}

runTest 函數首先會調用 waitUntilAppLoaded 函數,該函數的定義如下所示:

function waitUntilAppLoaded() {
  if (appLoaded == true) return true;
  else window.setTimeout(waitUntilAppLoaded, 100);
}

請記住,測試方案會將變數 appLoaded 初始化為 false,工具框架組 onload 事件會將 appLoaded 設置為 true。 此處,我使用內部 setTimeout 函數來重複暫停 100 毫秒,直到 appLoaded 的值變為 true。 請注意,此方法可能造成永久性延遲。 若要避免這種可能,您可能希望在最大延遲次數之後添加一個全域計數器並返回 false。

設置全域 start 變數後,runTest 將在例外處理常式包裝中顯示某些注釋並調用 step1 函數。 我在此處介紹的工具結構只有一種,您可根據您的程式設計風格和測試環境來修改工具組織。 在我的結構中,我會將測試方案當作一系列狀態更改,每次更改由 stepX 函數表示。

step1 函數通過類比使用者輸入來處理待測試的 Web 應用程式的狀態,如圖 4 所示。

圖 4 使用 step1 函數進行類比輸入

function step1() {
  logRemark(
    "Entering 3 and 5 and selecting Addition");
  var tb1 = 
    $(parent.rightFrame.document).find('#TextBox1');
  tb1.val('3');
  var tb2 = 
    $(parent.rightFrame.document).find('#TextBox2');
  tb2.val('5');
  var rb1 = 
    $(parent.rightFrame.document).find('#RadioButton1');
  rb1.attr("checked", true);
  logRemark(
    "\nClicking Calculate, waiting for async response '8.0000'");
  callAndWait(clickCalculate, checkControl, "TextBox3", "8.0000", 
    step2, polling);
}

用於訪問和處理 HTML 元素的 jQuery 語法具備一致性和明確性,並且大部分都是獨立于流覽器的。 請注意,若要通過 leftFrame 元素中的代碼訪問 rightFrame 元素中載入的 Web 應用程式,則必須使用 parent 關鍵字。 還請注意,必須使用 jQuery 查找篩選器。

操作 TextBox1 和 TextBox2 元素時,我假定已將待測試的 Web 應用程式完全載入到了 rightFrame 元素中。 對於載入時間較長的應用程式,此假設可能不合理,在這種情況下,您可將 jQuery 選擇器代碼放置到 window.setTimeout 延遲迴圈中,以便對內置的“未定義”值測試目標物件。

由於待測試的 MiniCalc 應用程式是一個 AJAX 應用程式,所以我的工具不能只調用“Calculate”(計算)按鈕按一下事件,這是因為測試工具代碼將不會等待應用程式的非同步回應,而是繼續執行。 因此,我使用了一個程式定義的 callAndWait 函數:

function callAndWait(action, checkControlFunc, controlID,
  controlVal, callbackFunc, pollTime) {
    numTries = 0;
    action();
    window.setTimeout(function(){doWait(
      checkControlFunc, controlID, controlVal, 
      callbackFunc, pollTime);}, pollTime);
}

callAndWait 函數將調用一個函數(action 參數),進入延遲迴圈,並暫停一段很短的時間(變數 pollTime),然後通過調用帶有形參 controlID 和 controlVal 的參數函數 checkControlFunc 來檢查某些應用程式的狀態是否為 true。 當 checkControlFunc 返回 true,或者已執行延遲的最大次數時,系統會將控制權轉交給參數函數 callbackFunc。

callAndWait 函數將與程式定義的 doWait 函數結合使用:

function doWait(checkControlFunc, controlID, 
  controlVal, callbackFunc, pollTime) {
  ++numTries;
  if (numTries > maxTries) finish();
  else  if (checkControlFunc(controlID, controlVal)) 
    callbackFunc();
  else window.setTimeout(function(){
    doWait(checkControlFunc, controlID,
    controlVal, callbackFunc, pollTime);}, pollTime);
}

當 checkControlFunc 返回 true 或本地計數器 numTries 超出全域變數 maxTries 時,doWait 函數將出現遞迴並將退出。 因此,這將調用一個名為 clickCalculate 的函數,進入延遲迴圈,暫停輪詢 500 毫秒並調用帶有參數 TextBox3 和 8.0000 的函數 checkControl,直到 checkControl 返回 true 或延遲迴圈執行 20 次(由 maxTries 指定):

callAndWait(clickCalculate, checkControl, "TextBox3",   "8.0000", step2, polling);

如果 checkControl 返回 true,則會將控制權轉交給函數 step2。 clickCalulate 函數使用 jQuery 選擇和連結:

function clickCalculate() {
  var btn1 = $(parent.rightFrame.document).find('#Button1');
  if (btn1 == null || btn1.val() == undefined) 
    throw "Did not find btn1";
  btn1.click();
}

按照此方法定義操作包裝函數的主要原因是便於將該函數按名稱傳遞到 callAndWait 函數。 checkControl 函數很簡單:

function checkControl(controlID, controlVal) {
  var ctrl = $(parent.rightFrame.document).find('#' + controlID);
  if (ctrl == null || ctrl.val() == undefined || ctrl.val() == "")
    return false;
  else
    return (ctrl.val() == controlVal);
}

首先,我使用 jQuery 語法來獲取對參數 controlID 所指定的控制項的引用。 如果該控制項的值尚不可用,則立即返回到延遲迴圈。 一旦該控制項值準備就緒,就可以檢查其是否與參數 controlVal 提供的某些預期值相等。

在調用我想調用的數量的 stepX 函數之後,我會將控制權轉交給 finish 函數。 該函數首先會確定它是如何被調用的:

if (numTries > maxTries) {
  logRemark("\nnumTries has exceeded maxTries");
  logRemark("\n*FAIL*");
}
else ....

如果全域 numTries 變數的值超過 maxTries 的值,則我就知道待測試的 Web 應用程式在允許的時間內未作出回應。 這裡,我斷定這是測試用例失敗,而不是某種形式的不確定結果。 如果 numTries 尚未超過 maxTries,則我會開始檢查待測試的應用程式的最終狀態:

logRemark("\nChecking final state");
var tb1 = $(parent.rightFrame.document).find('#TextBox1');
var tb2 = $(parent.rightFrame.document).find('#TextBox2');
var tb3 = $(parent.rightFrame.document).find('#TextBox3');

此處,我獲得了對三個文字方塊控制項的引用。 事實上,您決定要檢查的待測試的 Web 應用程式的元素將具體取決於特定應用程式的詳細資訊。 接下來,我會檢查每個文字方塊控制項的值,以查看是否每個控制項都得到了預期值:

var result = "pass";
if (tb1.val() != "3") result = "fail";
if (tb2.val() != "5") result = "fail";
if (tb3.val() != "8.0000") result = "fail";

我的測試方案腳本硬編碼了所有測試案例輸入和預期值。 我介紹的測試自動化最適合輕型和快速測試情形,其中的硬編碼測試資料簡單而有效。

finish 函數通過顯示“通過”或“未通過”結果來總結測試運行:

if (result == 'pass')
  logRemark("\n*Pass*");
else
  logRemark("\n*FAIL*");

與測試用例輸入資料一樣,此方法十分輕便,您可能想要在測試主機或 Web 伺服器上將測試結果寫入到外部檔,或者通過 SMTP 將測試結果發送到電子郵寄地址。

總結

此處所介紹的工具為半自動化工具,因為您必須按一下按鈕控制項才能啟用測試。 您可通過添加一個 start-wrapper 函數使該工具完全實現自動化:

function launch() {
  if (!started)
    runTest();
}

向工具頁中的框架組元素中添加屬性 onload=“leftFrame.launch();”。 工具中每次 Web 應用程式的載入都將觸發一個 onload 事件,因此我使用全域“start”變數來阻止測試自動化重新開機。 有趣的是,即使 HTML Frame 元素不支援 onload 事件,您實際上仍可在工具框架元素中放置一個 onload 屬性,該事件將會從其父框架組元素中冒出。

現在,您可使用以下命令創建 .bat 檔:

iexplore http://localhost/TestWithJQuery/UITestHarness001.html
iexplore http://localhost/TestWithJQuery/UITestHarness002.html

執行 .bat 檔後(可能通過 Windows 任務計畫程式執行),工具將會載入,您的自動化將會自動啟動。另一種擴展我在此處介紹的測試系統的方法是將程式定義的函數放置到 jQuery 外掛程式中。

在編寫輕型 Web 應用程式 UI 測試自動化時,對於我在此處介紹的基於 jQuery 的方法,您可有多種選擇。與使用原始 JavaScript 相比,使用 jQuery 庫的主要優點就是 jQuery 可跨多台流覽器(如 Internet Explorer、Firefox 和 Safari)工作。另一個顯著的優點就是,通過使用 jQuery 編寫測試自動化,您可主動構建對 Web 開發任務使用 jQuery 這方面的知識。

與其他方法相比,使用 jQuery 確實存在劣勢。使用 jQuery 在某種程度上需要一個外部依賴項,並且與非腳本測試自動化相比,基於腳本的測試自動化在管理上往往更加困難。與使用 Selenium 或 Watir 等測試框架相比,編寫基於 jQuery 的自動化可提供更大的靈活性,但您必須在更低的抽象級別上編寫代碼。

與往常一樣,我要提醒您,沒有任何一種特定的測試自動化方法能適用于所有情況,但是,在許多軟體發展方案中,基於 jQuery 的 Web 應用程式 UI 測試自動化可能是有效和高效的技術。

James McCaffrey 博士供職于 Volt Information Sciences, Inc.,在該公司他負責管理對華盛頓州雷蒙德市沃什灣 Microsoft 總部園區的軟體工程師進行的技術培訓。他參與過多項 Microsoft 產品的研發工作,包括 Internet Explorer 和 MSN Search。McCaffrey 博士是《.NET Test Automation Recipes》(Apress,2006)一書的作者,您可以通過以下位址與他聯繫:jammc@microsoft.com

衷心感謝以下技術專家對本文的審閱: Scott Hanselman 和 Matthew Osborn