ASP.NET

ASP.NET Web Form 架構導覽的單元測試

Graham Mendick

下載代碼示例

下載庫

在 ASP.NET Web 表單框架的導航,開放原始碼專案主辦 navigation.codeplex.com,開闢了新的可能性,導航和傳遞的資料以一種新方法編寫 Web 表單應用程式。在傳統的 Web 表單代碼中,依賴于執行的導航資料傳遞的方式。例如,它可能會舉行期間重定向,查詢字串或路由資料中,但控制項的值或開機自檢過程中的檢視狀態中回來。然而,在 ASP.NET Web 表單框架 ("導航框架"以下簡稱為簡潔起見) 的導航單個資料來源用在所有的情況。

我第一篇文章 (msdn.microsoft.com/magazine/hh975349),介紹了導航框架,並建立一個示例線上調查應用程式,以證明其關鍵的概念和的優勢,它提供了一些。特別是,我展示如何啟用它生成的一組動態、 上下文相關的軌跡瀏覽超連結允許使用者在返回到以前訪問過的問題,使用者的答案還原,克服靜態的 ASP.NET 網站映射功能的局限性。

此外在這第一篇文章,我聲稱導航框架讓您編寫會使應用程式的 ASP.NET MVC 嫉妒的 Web 表單代碼。然而,樣本測量中的應用才證明這項索賠由於代碼的擠迫到 codebehinds 和令人費解的單元測試。

我會讓事宜直在這第二條中編輯測量中的應用,所以,它只是為晉級為典型的 ASP.NET MVC 應用程式和具有較高水準的單元測試。我將使用標準 ASP.NET 資料繫結導航框架來清除 codebehinds 並解壓到一個單獨的類,我會那麼的單元測試的業務邏輯。這一測試不會要求任何嘲諷和代碼覆蓋率將包含導航邏輯 — — 很少提供其他 ASP.NET 單元測試方法的功能。

資料繫結

Web 表單代碼中的墓地充斥著浮腫屍體的隱藏檔,但它並不一定要通過這種方式。雖然 Web 表單具有特色自成立以來的資料繫結,它是 2005 年 Visual Studio 介紹允許 Web 表單應用程式開發更近乎于典型的 MVC 應用程式結構中的資料來源控制項和雙向的可更新綁定,綁定語法。這樣的代碼,尤其是單元測試,會帶來有益影響廣泛認識到,反映了這一事實已在這方面花了大部分的下一個版本的 Visual Studio 的 Web 表單發展努力。

為了演示,我採取調查應用程式開發的第一篇文章並將其轉換到 MVC 類似的體系結構。控制器類將舉行業務邏輯和 ViewModel 類將舉行控制器和視圖之間的通信的資料。因為目前在 codebehinds 中的代碼可以剪切和粘貼幾乎逐字到控制器,它將需要很少的發展努力。

從 Question1.aspx 開始,第一步是創建一個包含字串屬性,以便可以通過選定的答案,並從控制器的問題 ViewModel 類:

public class Question
{
  public string Answer
  {
    get;
    set;
  }
}

接下來是控制器類,其中 SurveyController ; 打電話了。 平原老 CLR 物件,不同于 MVC 控制器。 Question1.aspx 需要兩個方法,一個用於返回問題 ViewModel 類的資料檢索,一個用於接受質詢 ViewModel 類的資料更新:

public class SurveyController
{
  public Question GetQuestion1()
  {
    return null;
  }
  public void UpdateQuestion1(Question question)
  {
  }
}

要充實這些方法,我將使用代碼中的代碼隱藏中的 Question1.aspx,移動頁面載入到 GetQuestion1 的邏輯和按鈕點擊 UpdateQuestion1 進入的處理邏輯。 控制器不能訪問頁上的控制項,因為問題 ViewModel 類用於獲取和設置答案,而不是選項按鈕清單。 GetQuestion1 方法需要進一步的調整,以確保返回的預設回答是"Web 表單":

public Question GetQuestion1()
{
  string answer = "Web Forms";
  if (StateContext.Data["answer"] != null)
  {
    answer = (string)StateContext.Data["answer"];
  }
  return new Question() { Answer = answer };
}

在 MVC,資料繫結是在請求級別與請求映射到控制器的方法,通過路由註冊,但 Web 表單中,資料繫結是在控制水準與映射完成使用預。 所以要將 Question1.aspx 掛鉤到的 SurveyController 方法,我就會添加 FormView 連接到適當配置的資料來源中的一種:

<asp:FormView ID="Question" runat="server"
  DataSourceID="QuestionDataSource" DefaultMode="Edit">
  <EditItemTemplate>
  </EditItemTemplate>
</asp:FormView>
<asp:ObjectDataSource ID="QuestionDataSource" 
  runat="server" SelectMethod="GetQuestion1" 
  UpdateMethod="UpdateQuestion1" TypeName="Survey.SurveyController"  
  DataObjectTypeName="Survey.Question" />

最後一步是要移動的問題,由選項按鈕清單和按鈕,裡面的 FormView EditItemTemplate 組成。 同時,兩個必須進行更改順序的資料繫結機制工作。 第一次是使用的綁定語法,因此從 GetQuestion1 返回的答案顯示和新選的答案傳遞回 UpdateQuestion1。 第二個是要將按鈕的 CommandName 設置為更新,所以它被按下時,將自動調用 UpdateQuestion1 (您會注意到的第一個清單項的選定屬性已被刪除,因為在 GetQuestion1 中設置預設回答到"Web 表單"被現在的管理):

<asp:RadioButtonList ID="Answer" runat="server"
  SelectedValue='<%# Bind("Answer") %>'>
  <asp:ListItem Text="Web Forms" />
  <asp:ListItem Text="MVC" />
</asp:RadioButtonList>
<asp:Button ID="Next" runat="server" 
  Text="Next" CommandName="Update" />

過程已完成的 Question1.aspx,並且其隱藏 gratifyingly 為空。 可以遵循相同的步驟,將資料繫結添加到 Question2.aspx,但因為相關的後退導航超連結的頁面載入代碼時必須留在那兒,其代碼隱藏中不能完全清除。 在下一節,其中討論了與資料繫結的導航框架組成將標記和騰空的代碼隱藏中移動。

添加資料繫結到 Thanks.aspx 類似,但是,而不是重複使用不當命名的問題 ViewModel 類,會創建新的一個叫做的摘要與字串屬性來保存所選的答案:

public class Summary
{
  public string Text
  {
    get;
    set;
  }
}

因為 Thanks.aspx 是一個唯讀的螢幕,只有資料檢索方法所需的控制器上,並與 Question2.aspx,所有頁面載入除了後退導航邏輯的代碼可以移動到此方法:

public Summary GetSummary()
{
  Summary summary = new Summary();
  summary.Text = (string)StateContext.Data["technology"];
  if (StateContext.Data["navigation"] != null)
  {
    summary.Text += ", " + (bool)StateContext.Data["navigation"];
  }
  return summary;
}

沒有更新功能是必需的因為 FormView ItemTemplate 用於 EditItemTemplate,和語法的單向的約束力,Eval,用於代替綁定:

<asp:FormView ID="Summary" runat="server" 
  DataSourceID="SummaryDataSource">
  <ItemTemplate>
    <asp:Label ID="Details" runat="server" 
      Text='<%# Eval("Text") %>' />
  </ItemTemplate>
</asp:FormView>
<asp:ObjectDataSource ID="SummaryDataSource" 
  runat="server"
  SelectMethod="GetSummary" 
  TypeName="Survey.SurveyController" />

因為調查應用程式的業務邏輯已經提取到一個單獨的類,就是贏得了單元測試成功的一半。 然而,因為代碼已被粘貼到幾乎不變從隱藏的控制器,資料繫結的權力不是尚未充分利用。

導航資料繫結

調查應用程式代碼仍有幾個問題:僅在 SurveyController 中的更新方法應包含導航邏輯,與 codebehinds 不空。 直到這些問題已得到解決,因為前者會造成不必要的複雜的單元測試的 get 方法,後者會阻礙 100%的單元測試覆蓋率,不應該開始的單元測試。

選擇參數的資料來源控制項使訪問冗余的資料繫結方法中的 HttpRequest 物件。 例如,QueryStringParameter 類允許查詢字串資料作為參數傳遞給資料繫結方法。 導航框架具有 NavigationDataParameter 類,它執行相同的工作,對 StateCoNtext 物件的狀態資料。

配備此 NavigationDataParameter,我可以重新訪問 GetQuestion1,刪除所有通過使答案方法參數而訪問的狀態資料的代碼。 這大大簡化了代碼:

public Question GetQuestion1(string answer)
{
  return new Question() { Answer = answer ?? "
Web Forms" };
}

伴隨改為 Question1.aspx,是要將 NavigationDataParameter 添加到其資料來源。 這涉及到第一次註冊的頁面頂部的導航命名空間:

<%@ Register assembly="Navigation" 
                       namespace="Navigation" 
                        tagprefix="nav" %>

然後可以將 NavigationDataParameter 添加到資料來源選擇參數:

<asp:ObjectDataSource ID="QuestionDataSource" runat="server"
  SelectMethod="GetQuestion1" UpdateMethod="UpdateQuestion1" 
  TypeName="Survey.SurveyController" 
  DataObjectTypeName="Survey.Question" >
  <SelectParameters>
    <nav:NavigationDataParameter Name="answer" />
  </SelectParameters>
</asp:ObjectDataSource>

現在的 GetQuestion1 方法,被剝奪了所有特定于 Web,很容易是代碼的單元測試。 同樣可以做為 GetQuestion2。

對於 GetSummary 方法,需要兩個參數,另一個用於每個回答。 第二個參數是一個布林值,以匹配資料如何通過 UpdateQuestion2,和它必須是可為空值,因為第二個問題並不總是要求:

public Summary GetSummary(string technology, bool?
navigation)
{
  Summary summary = new Summary();
  summary.Text = technology;
  if (navigation.HasValue)
  {
    summary.Text += ", " + navigation.Value;
  }
  return summary;
}

Thanks.aspx 的資料來源到相應的改變是增加了兩個 NavigationDataParameters:

<asp:ObjectDataSource ID="SummaryDataSource" runat="server"
  SelectMethod="GetSummary" TypeName="Survey.SurveyController" >
  <SelectParameters>
    <nav:NavigationDataParameter Name="technology" />
    <nav:NavigationDataParameter Name="navigation" />
  </SelectParameters>
</asp:ObjectDataSource>

調查應用程式代碼的第一個問題已被解決,因為現在只更新方法在控制器中的包含導航邏輯。

您也許還記得導航框架提高對靜態 breadcrumb 導航功能提供的 Web 表單網站映射中,跟蹤的美國與他們狀態資料訪問和建設所採取的使用者的實際路線的上下文相關的軌跡瀏覽痕跡。 構建在標記中的後退導航超連結 — — 無需隱藏 — — 導航框架提供了類似于 SiteMapPath 控制項的 CrumbTrailDataSource。 當使用 ListView 作為後盾的資料來源,CrumbTrailDataSource 返回的項清單,每個以前訪問過的國家,每個包含一個 NavigationLink URL 可以恢復到該狀態的上下文相關導航以一個。

我將使用此新的資料來源將 Question2.aspx 後退導航移動到它的標記。 首先,我將它添加 ListView 連接到 CrumbTrailDataSource:

<asp:ListView ID="Crumbs" runat="server" 
  DataSourceID="CrumbTrailDataSource">
  <LayoutTemplate>
    <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
  </LayoutTemplate>
  <ItemTemplate>
  </ItemTemplate>
</asp:ListView>
<nav:CrumbTrailDataSource ID="CrumbTrailDataSource" runat="server" />

下一步,我就會從 Question2.aspx 的代碼中刪除頁面載入代碼­背後,移動內部 ListView ItemTemplate 後退導航超連結和使用 Eval 綁定來填充的 NavigateUrl 屬性:

<asp:HyperLink ID="Question1" runat="server"
  NavigateUrl='<%# Eval("NavigationLink") %>' Text="Question 1"/>

您會注意到該超連結的文字屬性是硬編碼到"問題 1"。這非常非常適合 Question2.aspx 因為唯一可能後退導航是第一個問題。 不過,同樣不能說的 Thanks.aspx 因為很可能要返回到第一或第二個問題。 幸運的是,在 StateInfo.config 檔中輸入的導航配置允許標題屬性,以關聯到每個狀態,如:

<state key="Question1" page="~/Question1.aspx" title="Question 1">

然後 CrumbTrailDataSource 讓這個標題可用於資料繫結:

<asp:HyperLink ID="Question1" runat="server"
  NavigateUrl='<%# Eval("NavigationLink") %>' 
  Text='<%# Eval("Title") %>'/>

這些相同的更改應用到 Thanks.aspx 位址調查應用程式代碼的第二個問題,因為所有 codebehinds 現在是空的。 但是,如果 SurveyController 不能為單元測試,會浪費這一切努力。

單元測試

與現在很好地結構調查應用程式 — — codebehinds 為空且所有使用者介面邏輯是在頁標記中 — — 這是 SurveyController 類的單元測試的時間。 GetQuestion1、 GetQuestion2 和 GetSummary 的資料檢索方法顯然可以是單元測試,因為它們不包含任何特定于 Web 的代碼。 只有 UpdateQuestion1 和更新­問題方法目前的單元測試的挑戰。 沒有導航框架中,這兩種方法將包含路由和重定向的電話 — — 移動和 ASPX 頁面之間傳遞資料的傳統方法 — — 哪都引發異常時在 Web 環境中,並導致單元測試在第一關的旅行之外使用。 然而,在地方的導航框架,這兩種方法可完全單元測試而無需任何代碼更改或模仿物件。

對於初學者,就會創建單元測試專案的調查。 SurveyController 類中的任何方法內部按右鍵,然後選擇"創建單元測試..."功能表選項將創建一個專案,包括所需的參照和調查­ControllerTest 類。

您也許還記得導航框架需要各國和過渡,以便在 StateInfo.config 檔中配置的清單。 單元測試專案使用相同的導航配置的順序,必須部署的 Web 專案中的 StateInfo.config 檔中執行單元測試時。 為此,我按兩下 Local.testsettings 解決方案專案並選擇部署選項卡下的"啟用部署"核取方塊。 然後我會引用此 StateInfo.config 檔的 DeploymentItem 屬性與裝飾的 SurveyControllerTest 類:

[TestClass]
[DeploymentItem(@"Survey\StateInfo.config")]
public class SurveyControllerTest
{
}

下一步,必須將 app.config 檔添加到此部署的 StateInfo.config 檔指向的測試專案 (此配置還需要在 Web 專案中,但它將被自動添加由 NuGet 安裝):

<configuration>
  <configSections>
    <sectionGroup name="Navigation">
      <section name="StateInfo" type=
        "Navigation.StateInfoSectionHandler, Navigation" />
    </sectionGroup>
  </configSections>
  <Navigation>
    <StateInfo configSource="StateInfo.config" />
  </Navigation>
</configuration>

使用此配置到位,單元測試可以開始了。 我會跟著 aaa 級模式的構建單元測試:

  1. 排列:設置前提條件和測試資料。
  2. 行為:執行測試下的單位。
  3. 斷言:驗證結果。

從開始的 UpdateQuestion1 方法,我會展示什麼有需要在這三個步驟的每個測試的導航和導航框架中傳遞的資料的時候。

排列步驟設置該單元測試,創建下測試和需要向下測試方法傳遞的參數的物件。 對於 UpdateQuestion1,這意味著創建 SurveyController 和填充相關答案的問題。 不過,額外導航設置條件是必需的鏡像啟動 Web 應用程式時發生的導航。 調查的 Web 應用程式啟動時,導航框架截獲的啟動頁,Question1.aspx,請求,並導航至對話方塊中,其路徑屬性匹配此請求在 StateInfo.config 檔中:

<dialog key="Survey" initial="Question1" path="~/Question1.aspx">

導航使用對話方塊鍵轉到提到在其初始屬性中,從而達到問題 1 狀態時的狀態。 因為它不可能在單元測試中設置啟動頁,此對話方塊中導航,必須手動執行,並且是排列步驟中所需的額外條件:

StateController.Navigate("Survey");

法一步調用測試下的方法。 這只是涉及到通過其填充到 UpdateQuestion1 的答案的問題,所以不需要任何特定于導航的詳細資訊。

Assert 步將預期的值對結果進行比較。 驗證導航和資料傳遞的成果可以做導航框架中使用的類。 您也許還記得 StateCoNtext 提供國家航行期間傳遞通過其資料的屬性,使用 NavigationData 進行初始化的資料的訪問。 這可以用來驗證 UpdateQuestion1 傳遞到下一個狀態選擇的答案。 該斷言假設"Web 表單"傳遞到方法,所以,就變成了:

Assert.AreEqual("Web Forms", (string) StateContext.Data["technology"]);

StateCoNtext 還具有一個狀態屬性,用於跟蹤的目前狀態。 這可以用來檢查是否導航發生像預期的那樣 — — 例如,那傳遞到 UpdateQuestion1 的"Web 表單"應流覽到問題:

Assert.AreEqual("Question2", StateContext.State.Key);

StateCoNtext 持有的目前狀態和關聯的資料的詳細資訊,同時,麵包屑是以前訪問過的國家和他們的資料的等效類 — — 所謂因為每次使用者導航,一個新被添加到的痕跡線索。 此 breadcrumb 徑或碎屑的清單是通過 StateController 的麵包屑屬性訪問 (和是在前一節 CrumbTrailDataSource 的備份資料)。 我需要求助於此清單,檢查 UpdateQuestion1 存儲傳遞的答案其狀態資料中導航之前, 因為一旦發生導航,粉創建持有此狀態資料。 假設中傳遞的答案"Web 表單",可以驗證上第一個也是唯一粉的資料:

Assert.AreEqual("Web Forms", (string) StateController.Crumbs[0].Data["answer"]);

關於導航框架一直被寫入結構的單元測試的 aaa 級模式。 所有這些步驟都放在一起,以下是單元測試,以檢查達到問題狀態時是否通過"Web 表單"的答案後到 UpdateQuestion1 (具有清晰的單獨步驟之間插入一個空行):

[TestMethod]
public void UpdateQuestion1NavigatesToQuestion2IfAnswerIsWebForms()
{
  StateController.Navigate("Survey");
  SurveyController controller = new SurveyController();
  Question question = new Question() { Answer = "Web Forms" };
  controller.UpdateQuestion1(question);
  Assert.AreEqual("Question2", StateContext.State.Key);
}

雖然這是所有你需要能夠對單元測試不同的導航框架概念,值得繼續與 UpdateQuestion2,因為它有幾個在其排列和行為的步驟中的差異。 在其排列步驟中所需的導航條件是不同的因為,若要調用 UpdateQuestion2,當前的狀態應該是問題和目前狀態資料應包含"Web 表單"技術的答案。 在 Web 應用程式中此導航和資料傳遞被管理通過使用者介面中,因為使用者不能進展到第二個問題,沒有回答第一個問題的"Web 表單"。 然而,在單元測試環境中,這必須手動完成。 這涉及相同的對話方塊的導航 UpdateQuestion1 需達到問題 1 狀態後, 跟一個傳遞的下一步的轉型關鍵的導航,在 NavigationData 中的"Web 表單"回答:

StateController.Navigate("Survey");
StateController.Navigate(
  "Next", new NavigationData() { { "technology", "Web Forms" } });

UpdateQuestion2 的斷言步中的唯一差別來驗證其答案存儲在導航前狀態資料時。 時做這種檢查是為了 UpdateQuestion1,在清單中的第一次粉被使用,因為只有一個國家已訪問過,即問題 1。 然而,為 UpdateQuestion2,將有兩個麵包屑清單中由於問題 1 和問題都已達到。 麵包屑出現在他們正在訪問,所以問題是第二個條目,並進行必要的檢查將成為的順序清單中:

Assert.AreEqual("Yes", (string)StateController.Crumbs[1].Data["answer"]);

單元測試的 Web 表單代碼的雄心壯志取得了。 這樣做是使用標準的資料繫結導航框架的協助。 這是減少指令性比其他 ASP.NET 單元測試方法,因為該控制器還沒不得不繼承或實現任何框架類或介面,且其方法沒有返回任何特定框架類型。

然而是 MVC 嫉妒嗎?

MVC 應該會感覺有些嫉妒的痛苦,因為測量中的應用是只為晉級為典型的 MVC 應用程式,但具有較高水準的單元測試。 調查應用程式的導航代碼內的控制器方法顯示和測試以及業務邏輯的其餘。 在 MVC 應用程式中,導航代碼不被測試,因為它包含在控制器的方法,如 RedirectResult 的返回類型。 在導航框架的下一篇文章中,我會遵守不重複自己,或幹,建立一個搜尋引擎優化友好、 單頁面應用程式複合 MVC 的嫉妒的難以實現它的 MVC 等效的原則。

儘管如此,Web 表單資料繫結不會有沒有對應的 MVC 中存在的問題。 例如,很難使用依賴注入的控制器類,與 ViewModel 類的嵌套的類型不受支援。 但是 Web 表單從 MVC,汲取了不少,Visual Studio 的下一個版本將看到大大改進的 Web 表單資料繫結經驗。

還有更多到導航框架組成與資料繫結比如下所示。 例如,有一種資料傳呼機控制的 — — 與 ASP.NET DataPager 不同 — — 不需要掛鉤到一個控制項或需要單獨計數方法。 如果你找出更多有興趣,全面的文檔和示例代碼可在 navigation.codeplex.com

Graham Mendick 為 Web 表單的最大風扇,想要顯示它可以只為結構合理作為 ASP.NET MVC。他創作的 ASP.NET Web 表單的框架,他認為的導航 — 用於資料繫結時 — 可以注入 Web 表單的新生命。

由於下面的技術專家,檢討這篇文章:Scott Hanselman