[30天快速上手TDD][Day 28]TDD 實戰練習 part 2–第一個綠燈
前言
前面介紹完每一塊拼圖的概念之後,上一篇文章開始進入實戰演練。
實戰練習的範例,是透過一個網路銀行的範例專案,挑選了登入功能這個 user story ,在上一篇文章中我們完成了下面幾個部分:
- 如何從 PO的描述中,定義出 user story 與 acceptance test cases 。
- 如何建立 BDD 相關的 feature 與 scenario 。
- 如何透過 Selenium 來設計驗收測試程式。
- 如何結合 BDD 的 steps 與 Selenium.WebDriver 。
接下來這篇文章,則是要用最快速的方式,通過目前兩個還處於紅燈的 scenario 。
目前的進度
- 目前 feature 上的 scenario 如下:
Scenario: 當提款卡Id為1234時,輸入密碼為91時,驗證成功,導到index Given 在登入頁面 And 提款卡Id輸入"1234" And 密碼輸入"91" When 按下確認按鈕 Then 頁面url為"index.aspx" Scenario: 當提款卡Id為1234時,輸入密碼為1234時,驗證失敗,出現密碼錯誤 Given 在登入頁面 And 提款卡Id輸入"1234" And 密碼輸入"1234" When 按下確認按鈕 Then 呈現訊息為"密碼輸入錯誤"
- 目前的測試程式如下:
using System; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using TechTalk.SpecFlow; namespace TestWebBank { [Binding] public class 登入功能Steps { #region Test Setting private static IWebDriver driver; private static StringBuilder verificationErrors; private static string baseURL; [BeforeFeature("WebBank")] public static void BeforeFeatureWebAtm() { driver = new FirefoxDriver(); //請自行修改為網站的domain name與port baseURL = "http://localhost:10542"; verificationErrors = new StringBuilder(); } [AfterFeature("WebBank")] public static void AfterFeatureWebAtm() { try { driver.Quit(); } catch (Exception) { // Ignore errors if unable to close the browser } Assert.AreEqual("", verificationErrors.ToString()); } #endregion Test Setting [Given(@"在登入頁面")] public void Given在登入頁面() { driver.Navigate().GoToUrl(baseURL + "/WebBankSite/Login.aspx"); } [Given(@"提款卡Id輸入""(.*)""")] public void Given提款卡Id輸入(string cardId) { driver.FindElement(By.Id("txtCardId")).Clear(); driver.FindElement(By.Id("txtCardId")).SendKeys(cardId); } [Given(@"密碼輸入""(.*)""")] public void Given密碼輸入(string password) { driver.FindElement(By.Id("txtPassword")).Clear(); driver.FindElement(By.Id("txtPassword")).SendKeys(password); } [When(@"按下確認按鈕")] public void When按下確認按鈕() { driver.FindElement(By.Id("btnLogin")).Click(); } [Then(@"頁面url為""(.*)""")] public void Then頁面Url為(string url) { var expected = string.Format("{0}/WebBankSite/{1}", baseURL, url); Assert.AreEqual(expected, driver.Url); } [Then(@"呈現訊息為""(.*)""")] public void Then呈現訊息為(string message) { Assert.AreEqual(message, driver.FindElement(By.Id("Message")).Text); } } }
- 目前的production code 如下:
public partial class Login : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void btnLogin_Click(object sender, EventArgs e) { //密碼驗證錯誤 //this.Message.Text = @"密碼輸入錯誤"; //密碼驗證成功 //Response.Redirect("index.aspx"); } }
TDD 的原則:剛剛好的程式碼
TDD 的一個原則就是,當出現紅燈的時候,接下來撰寫 production code ,期望只寫出「剛好滿足」測試程式的 production code ,一行不多、一行不少。
這個原則在一開始接觸時,開發人員可能會很不習慣,尤其是幾乎每一本或每一篇從頭介紹 TDD 的書籍文章,都會先建議讀者先用最快、最笨的方式來實作出通過測試的 code 。
這看起來似乎很不 make sense ,但這會有個很特別的效果,就是又往目標前進一步的效果。也能收到在 TDD 循環中,擁有節奏的感覺。
不過,當需求明確且開發人員已經可以知道幾步以後的重構模樣,那建議就不必每次都從最一開始的 hard-code 開始,因為每一份重構的工,還是得花掉一些些時間。
這篇文章,仍舊先從最一開始 hard-code 來解說,以方便完全沒有相關經驗的讀者可以循序漸進。
依據 Scenario ,迅速通過測試
首先,針對 login success 的 scenario ,我們來撰寫 production code ,使其迅速通過測試。
Production code 程式碼如下:
protected void btnLogin_Click(object sender, EventArgs e)
{
//密碼驗證錯誤
//this.Message.Text = @"密碼輸入錯誤";
//Scenario: 當提款卡Id為1234時,輸入密碼為91時,驗證成功,導到index
//Given 在登入頁面
//And 提款卡Id輸入"1234"
//And 密碼輸入"91"
//When 按下確認按鈕
//Then 頁面url為"index.aspx"
//密碼驗證成功
var id = this.txtCardId.Text.Trim();
var password = this.txtPassword.Text;
if (id == "1234" && password == "91")
{
Response.Redirect("index.aspx");
}
}
一個 hard-code 的判斷式,讓我們通過了 login success 的scenario 。(但 login failed 的 scenario 仍舊是測試失敗)
如下圖所示:
接下來我們用同一個方式,想辦法先通過 login failed 的scenario 。
程式碼如下:
protected void btnLogin_Click(object sender, EventArgs e)
{
var id = this.txtCardId.Text.Trim();
var password = this.txtPassword.Text;
if (id == "1234" && password == "91")
{
//密碼驗證成功
Response.Redirect("index.aspx");
}
//Scenario: 當提款卡Id為1234時,輸入密碼為1234時,驗證失敗,出現密碼錯誤
//Given 在登入頁面
//And 提款卡Id輸入"1234"
//And 密碼輸入"1234"
//When 按下確認按鈕
//Then 呈現訊息為"密碼輸入錯誤"
if (id == "1234" && password == "1234")
{
//密碼驗證錯誤
this.Message.Text = @"密碼輸入錯誤";
}
}
OK,按照 Scenario 的描述,寫完剛好通過測試的程式碼,執行一下測試,可以看到,兩個 scenario 都通過了。如下圖所示:
很好,我們已經進入 TDD 的第二個階段:綠燈!
這時候,如果 scenario 已經能夠 100% 代表整個 user story 的話,就代表可以 deploy 了。(當然啦,在這個範例中,這樣的 scenario 根本不可能是 100% )
Anyway, 綠燈之後,接下來我們就要進行重構。
重構
有了測試的保護,我們在進行重構的過程中,仍須確保原本的綠燈不會因為重構的動作,而導致變成紅燈。這也是重構的起手式,務必先建立自動測試保護。讀者想了解細節的話,可以參考前面的文章: [30天快速上手TDD][Day 10]Refactoring 起手式 - 建立測試
垃圾分類
目前的程式碼已經有相關的註解了,我們可以先來進行垃圾分類。將 login success 的處理與 login failed 的處理,用 extract method 的方式,擷取成 function 。
程式碼如下:
protected void btnLogin_Click(object sender, EventArgs e)
{
var id = this.txtCardId.Text.Trim();
var password = this.txtPassword.Text;
if (id == "1234" && password == "91")
{
LoginSuccess();
}
if (id == "1234" && password == "1234")
{
LoginFailed();
}
}
/// <summary>
/// 密碼驗證錯誤
/// </summary>
private void LoginFailed()
{
this.Message.Text = @"密碼輸入錯誤";
}
/// <summary>
/// 密碼驗證成功
/// </summary>
private void LoginSuccess()
{
Response.Redirect("index.aspx");
}
執行測試,仍是綠燈,通過測試。
職責分離
Authentication 這一件事,不應該交由頁面來決定,而是該由特定的 Authentication class 來處理。
這邊建議至少按照 3-layer 的方式,將相關的 BLL, DAL 抽離 PL ,因此 Authentication class 會放到新建的 library 中)
所以定義出負責 Authentication 的 class 之後,程式碼如下:
protected void btnLogin_Click(object sender, EventArgs e)
{
var id = this.txtCardId.Text.Trim();
var password = this.txtPassword.Text;
var authentication = new Authentication();
bool isValid = authentication.Verify(id, password);
if (isValid)
{
LoginSuccess();
}
else
{
LoginFailed();
}
//if (id == "1234" && password == "91")
//{
// LoginSuccess();
//}
//if (id == "1234" && password == "1234")
//{
// LoginFailed();
//}
}
/// <summary>
/// 密碼驗證錯誤
/// </summary>
private void LoginFailed()
{
this.Message.Text = @"密碼輸入錯誤";
}
/// <summary>
/// 密碼驗證成功
/// </summary>
private void LoginSuccess()
{
Response.Redirect("index.aspx");
}
我們一樣透過 Visual Studio 的產生功能,來幫我們產生對應的 class 與 function 。
這時候執行測試,我們會得到測試失敗的結果,原因是 NotImplementedException
的 exception 。
建立單元測試
我們有了 library , Authentication 的 class , Verify 的方法。也知道了期望的結果,接下來在寫 Verify 方法的內容之前,我們用一樣的方式先來建立測試案例與測試程式。
我們建立一個 Authentication 的 feature ,其 Scenario 如下圖所示:
有了 Scenario ,接下來完成我們的測試程式。
Authentication 的測試程式
測試程式就依照 Scenario 的 template來撰寫。想了解細節的讀者,可以參考前面的文章:
程式碼如下:
[Binding]
public class AuthenticationSteps
{
private static Authentication target;
[BeforeScenario("Authentication")]
public static void BeforeFeatureAuthentication()
{
target = new Authentication();
ScenarioContext.Current.Clear();
}
[AfterScenario("Authentication")]
public static void AfterFeatureAuthentication()
{
ScenarioContext.Current.Clear();
}
[Given(@"id為""(.*)""")]
public void GivenId為(string id)
{
ScenarioContext.Current.Add("id", id);
}
[Given(@"password為""(.*)""")]
public void GivenPassword為(string password)
{
ScenarioContext.Current.Add("password", password);
}
[When(@"呼叫Verify")]
public void When呼叫Verify()
{
var id = ScenarioContext.Current["id"].ToString();
var password = ScenarioContext.Current["password"].ToString();
var result = target.Verify(id, password);
ScenarioContext.Current.Add("result", result);
}
[Then(@"回傳""(.*)""")]
public void Then回傳(string result)
{
var isValid = Convert.ToBoolean(result);
var actual = Convert.ToBoolean(ScenarioContext.Current["result"]);
Assert.AreEqual(isValid, actual);
}
}
這時候一樣執行測試會失敗,因為 production code 還沒開始實作。
撰寫 Authentication ,以通過相關測試案例
接下來,只要把原本放在頁面上的程式,轉移到 Authentication 的 Verify 方法中即可。
這個手法,可以參考前面文章: [30天快速上手TDD][Day 15]Refactoring - 食神歸位
程式碼如下:
public class Authentication
{
public bool Verify(string id, string password)
{
if (id == "1234" && password == "91")
{
//LoginSuccess();
return true;
}
if (id == "1234" && password == "1234")
{
//LoginFailed();
return false;
}
return false;
}
}
執行一下測試,可以看到四個測試案例都通過了。如下圖所示:
小結
OK ,整個步驟到這邊,我們已經從 acceptance testing (透過 Selenium.WebDriver ),往下 drill down 到integration testing 了,也就是 Authentication 物件的測試。
整體的程式碼,演變過程如下:
- 沒有任何實作內容。
- 在頁面上加入 hard-code 的兩個判斷式,通過 acceptance testing 。
- 重構成 LoginSuccess() 與 LoginFailed() 兩個 function。
- 職責分離,建立 Authentication class , Verify 的方法。
- 建立 Authentication 的 feature 與 scenario (屬於物件層級的 feature )
- 建立 Authentication 的測試程式
- 實作 Authentication 的 Verify 方法內容,將頁面的邏輯判斷移到 Verify() 裡。
- 通過 Authentication 的測試,同時通過 Login Feature 上的測試。
現在我們已經有 Login 頁面的驗收測試案例,也有了 Authentication 物件的測試案例,並且擁有剛好可以通過 Login Feature 與 Authentication Feature 的 production code 。
下一篇文章,則要針對物件層級的 integration testing 與 unit testing ,以及運用 OOD/OOP 的基本原則,來讓整個設計更臻完善,都完成後,就能開始挑選下一個 user story 。
blog 與課程更新內容,請前往新站位置:http://tdd.best/