到目前為止,您已熟 悉我的偏好,即邀請開發人員到我在 Vermont 主持的用戶組發表我感興趣的話題。因此,產 生了有關 Knockout.js 和 Breeze.js 等主題的專欄。還有更多主題,如命令查詢職責分離 (CQRS),我已仔細研讀了一段時間。但最近架構師兼測試人員 Dennis Doire 談到 SpecFlow 和 Selenium,測試人員可使用這兩個工具進行行為驅動開發 (BDD)。我又一次睜大了眼睛, 開始在心裡尋找玩耍這些工具的理由。話雖如此,我真正關注的還是 BDD。雖然我是數據驅 動方面的工作人員,但我從數據庫向上開發應用程序的日子已經一去不返,我對關注這個領 域開始感興趣。
BDD 是測試驅動開發 (TDD) 的一種變化形式,它關注用戶情景以及圍繞這些情景建立邏 輯和測試。您不是要滿足一條規則,而是要滿足多組活動。它非常整體化,而我喜歡這一點 ,因此這個方面讓我產生了濃厚的興趣。這個理念在於,雖然典型的單元測試有可能確保客 戶對象的一個事件工作正常,而 BDD 關注的則是作為用戶的我在使用為我建立的系統時所預 期的更為廣泛的行為情景。BDD 經常在與客戶討論期間用於定義驗收條件。例如,當我坐在 計算機前,填寫“新建客戶”窗體,然後按“保存”按鈕時,系統應存儲客戶信息,然後向 我顯示一條消息,表示已成功存儲該客戶。
或者,可能當我激活軟件的“客戶管理”部分時,該部分應自動打開我在上一個會話中處 理的最新客戶。
從這些用戶情景中可發現,BDD 可能是一種面向 UI 的方法,用於設計自動測試,但在設 計 UI 之前將編寫許多方案。並且通過 Selenium (docs.seleniumhq.org) 和 WatiN (watin.org) 等工具,可在浏覽器中自動進行測試。但 BDD 不僅是描述用戶交互。若要詳細了解 BDD 圖片,請查看 InfoQ 上的小組討論、BDD 和 TDD 的某些官方機構以及 bit.ly/10jp6ve 上的 Specification by Example。
我想脫離對按鈕單擊等問題的困擾,少量地重新定義用戶情景。我可從情景中刪除依賴於 UI 的元素,而關注流程中不依賴於屏幕的部分。當然,我感興趣的是與數據訪問相關的情景 。
構建用於測試滿足特定行為的邏輯可能比較繁瑣。Doire 在他的演示中介紹的一個工具是 SpecFlow (specflow.org)。此工具與 Visual Studio 集成,可通 過此工具的簡單規則定義用戶情景(稱為方案)。然後,它自動創建和執行某些方法(有些 方法進行測試,有些方法不進行測試)。目標是驗證滿足情景的規則。
我將帶領您創建一些行為以引起您的興趣,如果您要詳細了解,可在本文結尾找到一些資 源。
首先,需要將 SpecFlow 裝入 Visual Studio,可從 Visual Studio 的“擴展和更新管 理器”中執行此操作。由於 BDD 的點是通過描述行為開始開發項目,因此解決方案中的第一 個項目是一個測試項目,將在該項目中描述這些行為。解決方案的其余部分將從該點繼續。
使用“單元測試項目”模板新建一個項目。項目需要引用 TechTalk.SpecFlow.dll,可使 用 NuGet 安裝該文件。然後,在此項目中創建一個名為 Features 的文件夾。
我的第一項功能將以有關添加新客戶的用戶情景為基礎,因此在 Features 文件夾中,我 創建了另一個名為 Add 的文件夾(見圖 1)。我將在此處定義方案並讓 SpecFlow 幫助我。
圖 1:具有 Features 和 Add 子文件夾的測試項目
SpecFlow 遵照一種依賴關鍵字的特定模式,這些關鍵字幫助描述要定義其行為的功能。 這些關鍵字來自一種名為 Gherkin(沒錯,就是泡菜裡的小黃瓜)的語言,而所有這些都起 源於一種名為 Cucumber (cukes.info) 的工具。其中一些關鍵字為 Given、And、When 和 Then,並且您可使用這些關鍵字生成方案。例如,下面是一個簡單的方案,封裝在“添加新 客戶”功能中:
Given a user has entered information about a customerWhen she completes entering more informationThen that customer should be stored in the system
可更詳細地描述,例如:
Given a user has entered information about a customerAnd she has provided a first name and a last name as requiredWhen she completes entering more informationThen that customer should be stored in the system
我將在這最後一個語句執行一些數據持久性。SpecFlow 並不關心其中進行任何一個過程 的方式。目標是編寫方案以證明結果是並且一直是成功的。該方案將驅動一組測試,而這些 測試將幫助您充實域邏輯:
Given that you have used the proper keywordsWhen you trigger SpecFlowThen a set of steps will be generated for you to populate with codeAnd a class file will be generated that will automate the execution of these steps on your behalf
我們來看一下這是如何工作的。
右鍵單擊 Add 文件夾以添加一個新項。如果已安裝 SpecFlow,則可通過搜索 specflow ,找到三個與 SpecFlow 相關的項。選擇 SpecFlow Feature File 項,然後向其提供一個名 稱。我將自己的該項命名為 AddCustomer.feature。
功能文件以示例(普遍存在的數學功能)開頭。注意,在頂部描述 Feature,在底部使用 Given、And、When 和 Then 描述 Scenario(表示功能的主要示例)。SpecFlow 加載項確保 文本經過彩色編碼,因此可輕松分辨步驟詞與您自己的語句。
我將用我自己的功能和步驟替換已有的功能和步驟:
Feature: Add CustomerAllow users to create and store new customersAs long as the new customers have a first and last nameScenario: HappyPathGiven a user has entered information about a customerAnd she has provided a first name and a last name as requiredWhen she completes entering more informationThen that customer should be stored in the system
(多虧 David Starr 提供了 Scenario 的名稱!我從他的 Pluralsight 視頻中借用了這 個名稱。)
如果未提供所需的數據會怎樣?我將在此功能中創建另一個方案以應對這種可能性:
Scenario: Missing Required DataGiven a user has entered information about a customerAnd she has not provided the first name and last nameWhen she completes entering more informationThen that user will be notified about the missing dataAnd the customer will not be stored into the system
這個方案將暫時處理這種情況。
到目前為止,您已了解 SpecFlow 提供的 Feature 項和彩色編碼。注意,有一個隱藏代 碼文件附加到功能文件,前者有一些根據功能創建的空測試。其中每個測試都將執行您的方 案中的步驟,但您確實需要創建這些步驟。有幾種方法可以做到這一點。可運行測試,然後 SpecFlow 將在測試輸出中返回 Steps 類的代碼列表供復制和粘貼。或者,可使用功能文件 的上下文菜單中的某個工具。我將介紹第二種方法:
在功能文件的文本編輯器窗口中單擊右鍵。在上下文菜單上,您將看到一個專用於 SpecFlow 任務的部分。
單擊“生成步驟定義”。隨後將彈出一個窗口,其中驗證要創建的步驟。
單擊“將方法復制到剪貼板”按鈕,然後使用默認值。
在項目中的 AddCustomer 文件夾中,新建一個名為 Steps.cs 的類文件。
打開文件,然後在類定義中,粘貼剪貼板內容。
使用 TechTalk.SpecFlow 向文件頂部添加一個命名空間引用。
向類添加綁定批注。
圖 2 列出這個新類。
圖 2:Steps.cs 文件
[Binding] public class Steps { [Given(@"a user has entered information about a customer")] public void GivenAUserHasEnteredInformationAboutACustomer() { ScenarioContext.Current.Pending(); } [Given(@"she has provided a first name and a last name as required")] public void GivenSheHasProvidedAFirstNameAndALastNameAsRequired () { ScenarioContext.Current.Pending(); } [When(@"she completes entering more information")] public void WhenSheCompletesEnteringMoreInformation() { ScenarioContext.Current.Pending(); } [Then(@"that customer should be stored in the system")] public void ThenThatCustomerShouldBeStoredInTheSystem() { ScenarioContext.Current.Pending(); } [Given(@"she has not provided both the firstname and lastname")] public void GivenSheHasNotProvidedBothTheFirstnameAndLastname() { ScenarioContext.Current.Pending(); } [Then(@"that user will get a message")] public void ThenThatUserWillGetAMessage() { ScenarioContext.Current.Pending(); } [Then(@"the customer will not be stored into the system")] public void ThenTheCustomerWillNotBeStoredIntoTheSystem() { ScenarioContext.Current.Pending(); } }
如果查看我創建的兩個方案,就會注意到,雖然在定義的內容中有一些重復(如 “a user has entered information about a customer”),但生成的方法不會創建 重復的步驟。還可注意到 SpecFlow 將利用方法特性中的常量。實際的方法名稱無關緊要。
此時,可讓 SpecFlow 運行其將調用這些方法的測試。雖然 SpecFlow 支持許多單元測試 框架,但我使用的是 MSTest,因此,如果在 Visual Studio 中查看此解決方案,就會發現 ,Feature 的代碼隱藏文件為每個方案都定義一個 TestMethod。每個 TestMethod 執行正確 的步驟方法組合以及一個為 HappyPath 方案運行的 TestMethod。
如果我現在要通過右鍵單擊功能文件,然後選擇“運行 SpecFlow 方案”運行此方案,則 測試將沒有明確結論,並顯示消息: “One or more step definitions are not implemented yet”(還有一個或多個步驟定義未實現)。這是因為 Steps 文件中的每種方 法都仍在調用 Scenario.Current.Pending。
因此,現在要充實這些方法。我的方案告知我,我將需要 Customer 類型和一些必要的數 據。根據其他文檔,我了解到當前需要名字和姓氏,因此我將在 Customer 類型中需要這兩 個屬性。我還需要一個用於存儲該客戶的機制以及一個存儲它的位置。我的測試不考慮其存 儲方式或位置,保留原樣即可,因此我將使用一個將負責獲得和存儲數據的存儲庫。
首先,向 Steps 類添加 _customer 和 _repository 變量:
private Customer _customer; private Repository _repository;
然後,去掉 Customer 類:
public class Customer { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
這樣即足以讓我向我的步驟方法添加代碼。圖 3 顯示添加到 HappyPath 相關步驟的邏輯。我在某一步新建一個客戶,然後在下一步提供所需的名字和姓 氏。實際上,不必做任何操作即可詳細說明 WhenSheCompletesEnteringMoreInformation 步 驟。
圖 3:某些 SpecFlow 步驟方法
[Given(@"a user has entered information about a customer")] public void GivenAUserHasEnteredInformationAboutACustomer() { _newCustomer = new Customer(); } [Given(@"she has provided a first name and a last name as required")] public void GivenSheHasProvidedTheRequiredData() { _newCustomer.FirstName = "Julie"; _newCustomer.LastName = "Lerman"; } [When(@"she completes entering more information")] public void WhenSheCompletesEnteringMoreInformation() { }
最後一步最令人關注。我在這一步不僅存儲客戶,還證明確實已存儲了它。我需要存儲庫 中有一個 Add 方法用於存儲客戶、一個 Save 用於將其推送到數據庫以及一種方法以查看存 儲庫能否實際找到該客戶。因此我將向存儲庫添加一個 Add 方法、一個 Save 方法和一個 FindById 方法,如下所示:
public class CustomerRepository { public void Add(Customer customer) { throw new NotImplementedException(); } public int Save() { throw new NotImplementedException(); } public Customer FindById(int id) { throw new NotImplementedException(); } }
現在,我可向 HappyPath 方案將調用的最後一步添加邏輯。我將向存儲庫添加該客戶, 然後通過測試,查看能否在存儲庫中找到該客戶。在這裡,我最後使用一個斷言判斷我的方 案是否成功。如果找到客戶(即 IsNotNull),則測試通過。這是測試已存儲這些數據的常 用模式。但是,憑借我對實體框架的經驗,我發現了一個無法通過測試發現的問題。我首先 將給出以下代碼,因此可用一種方式向您說明問題,而這種方式與僅向您告知正確的開始方 式(這樣有點不好意思)相比更容易記住:
[Then(@"that customer should be stored in the system")] public void ThenThatCustomerShouldBeStoredInTheSystem() { _repository = new CustomerRepository(); _repository.Add(_newCustomer); _repository.Save(); Assert.IsNotNull(_repository.FindById(_newCustomer.Id)); }
查看本欄目
再次運行 HappyPath 測試時,該測試失敗。可在圖 4 中看到測試輸 出顯示我的 SpecFlow 方案到現在為止如何工作。但請注意測試失敗的原因: 不是因為 FindById 未找到客戶,而是因為尚未實現我的存儲庫方法。
圖 4:失敗的測試所產生的輸出,其中顯示每一步的狀態
Test Name: HappyPath Test Outcome: Failed Result Message: Test method UnitTestProject1.UserStories.Add.AddCustomerFeature.HappyPath threw exception: System.NotImplementedException: The method or operation is not implemented. Result StandardOutput: Given a user has entered information about a customer -> done: Steps.GivenAUserHasEnteredInformationAboutACustomer() (0.0s) And she has provided a first name and a last name as required -> done: Steps. GivenSheHasProvidedAFirstNameAndALastNameAsRequired() (0.0s) When she completes entering more information -> done: Steps.WhenSheCompletesEnteringMoreInformation() (0.0s) Then that customer should be stored in the system -> error: The method or operation is not implemented.
那麼,下一步是向我的存儲庫提供邏輯。最後,我將使用此存儲庫與數據庫進行交互,由 於我碰巧是實體框架的愛好者,因此我將在我的存儲庫中使用實體框架 DbContext。首先, 我將創建一個 DbContext 類,它公開 Customers DbSet:
public class CustomerContext:DbContext { public DbSet<Customer> Customers { get; set; } }
然後,我可重構我的 CustomerRepository 以使用 CustomerContext 進行暫留。對於此 演示,我將直接對照上下文進行工作而不關注抽象。以下是經過更新的 CustomerRepository :
public class CustomerRepository { private CustomerContext _context = new CustomerContext(); public void Add(Customer customer { _context.Customers.Add(customer); } public int Save() { return _context.SaveChanges(); } public Customer FindById(int id) { return _context.Customers.Find(id); } }
現在,當我重新運行 HappyPath 測試時,該測試通過,並且我的所有步驟均被標為已完 成。但我仍不滿意。
為什麼我的測試通過並且看到漂亮的綠色圓圈時還不滿足?因為我知道該測試並非確實證 明存儲了客戶。
在 ThenThatCustomerShouldBeStoredInTheSystem 方法中,將對 Save 的調用注釋掉, 然後再次運行該測試。它仍可通過測試。並且,我甚至沒有將客戶保存到數據庫中!現在, 您是否察覺到什麼不正常的情況?這正是所謂的“誤報”。
問題在於我在存儲庫中使用的 DbSet Find 方法是實體框架中的一個特殊方法,它首先檢 查由上下文跟蹤的內存中對象,然後再轉到數據庫。當我調用 Add 時,就使 CustomerContext 感知到該客戶實例。對 Customers.Find 的調用發現該實例,並跳過對數 據庫執行無意義的操作。實際上,客戶的 ID 仍為 0,因為尚未保存它。
那麼,由於我使用實體框架(應將所使用的任何對象關系映射 [ORM] 框架的行為考慮在 內),因此我有一種更簡單的方式可了解客戶是否確實存入數據庫。當 EF SaveChanges 指 令將客戶插入數據庫時,它將取回由新數據庫生成的客戶 ID,然後將其應用於它插入的實例 。因此,如果新客戶的 ID 不再為 0,那麼我就知道我的客戶確實已存入數據庫。我不必重 新查詢數據庫。
我將相應地修改該方法的 Assert。以下是我所知道的將進行適當測試的方法:
[Then(@"that customer should be stored in the system")] public void ThenThatCustomerShouldBeStoredInTheSystem() { _repository = new CustomerRepository(); _repository.Add(_newCustomer); _repository.Save(); Assert.IsNotNull(_newCustomer.Id>0); }
該測試通過,並且我知道它因為正確的原因而通過。定義的測試無法通過的情況並不少見 ,例如,使用 Assert.IsNull(FindById(customer.Id) 確保您不是因為錯誤的原因而未通過 。但在這種情況下,直到我刪除對 Save 的調用後,問題才會顯現。如果對 EF 的工作方式 沒有把握,那麼最好還要創建一些與用戶情景無關的特定集成測試,以確保存儲庫表現出的 行為符合預期。
在我仔細研究有關理解這第一個 SpecFlow 方案的學習曲線時,我遇到了急轉直下的情況 。我的方案規定客戶應存儲在“系統”中。
問題在於我對系統的定義沒有把握。我的職業經歷告訴我,數據庫或至少某些持久性機制 是系統中非常重要的一部分。
但用戶不關注存儲庫和數據庫 - 而只關注其應用程序。但是,如果用戶重新登錄其應用 程序,卻因客戶並未真正存儲到數據庫中(因為我覺得 _repository.Save 對於實現其方案 並非必要)而無法再次找到該客戶,那麼用戶不會很高興。
我請教了另一位 Dennis,Dennis Doomen,他是 Fluent Assertions 的作者,多次參與 過大型企業系統的 BDD、TDD 等項目。他確認,我作為開發人員,肯定應該將我的知識應用 於步驟和測試,即使這意味著超出定義原始方案的用戶的意圖也是如此。用戶提供其知識, 而我加入我的知識,但不要將我的技術觀點強加於用戶。我繼續以用戶能聽懂的方式交談, 於是與用戶的交流暢通無阻。
我深知,如果沒有所有這些為支持 BDD 而開發的工具,我學習它的過程不會這麼輕松。 雖然我是一個數據極客,但我非常注重與客戶協同工作、了解其業務並確保其使用我幫助為 其開發的軟件時擁有快樂的體驗。這正是領域驅動設計和行為驅動設計對我如此重要的原因 。我覺得許多開發人員與我感同身受,甚至感觸更深,並且也可受到這些方法的啟發。
除了一路走來曾幫助過我的朋友以外,以下是我找到的一些有用資源。MSDN 雜志文章“ 使用 SpecFlow 和 WatiN 進行行為驅動開發”很有助益,可在 msdn.microsoft.com/magazine/gg 490346 上找到它。我還觀看了 David Starr 在 Pluralsight.com 上 Test First Development 課程 中一個非常好的單元。(實際上,我觀看了該單元許多次。) 我發現維基百科上有關 BDD 的條目 (bit.ly/LCgkxf) 令人感興趣,其中詳細介紹了 BDD 的 歷史以及它適合哪些其他做法。此外,我熱切盼望 Paul Rayner(他也曾建議我到這兒來) 與人合著的“BDD and Cucumber”這本書的出版上市。
下載代碼示例