iT邦幫忙

DAY 28
3

30天快速上手TDD系列 第 28

[Day 28]TDD實戰練習-2

前面介紹完每一塊拼圖的概念之後,上一篇文章開始進入實戰演練。

實戰練習的範例,是透過一個網路銀行的範例專案,挑選了登入功能這個user story,在上一篇文章中我們完成了下面幾個部分:

  1. 如何從PO的描述中,定義出user story與acceptance test cases。
  2. 如何建立BDD相關的feature與scenario。
  3. 如何透過Selenium來設計驗收測試程式。
  4. 如何結合BDD的steps與Selenium.WebDriver。

接下來這篇文章,則是要用最快速的方式,通過目前兩個還處於紅燈的scenario。

上一篇文章:[Day 27]TDD實戰練習-1
本系列文章專區
@目前的進度

  1. 目前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 呈現訊息為"密碼輸入錯誤"

  2. 目前的測試程式如下:

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

    }

  3. 目前的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, 綠燈之後,接下來我們就要進行重構。

@重構
有了測試的保護,我們在進行重構的過程中,仍須確保原本的綠燈不會因為重構的動作,而導致變成紅燈。這也是重構的起手式,務必先建立自動測試保護。讀者想了解細節的話,可以參考前面的文章:[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來撰寫。想了解細節的讀者,可以參考前面的文章:

  1. [Day 24]BDD - SpecFlow Introduction
  2. [Day 25]BDD - TDD from BDD

程式碼如下:

    [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方法中即可。
這個手法,可以參考前面文章:[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物件的測試。

整體的程式碼,演變過程如下:
0. 沒有任何實作內容。

  1. 在頁面上加入hard-code的兩個判斷式,通過acceptance testing。
  2. 重構成LoginSuccess()與LoginFailed()兩個function。
  3. 職責分離,建立Authentication class,Verify的方法。
  4. 建立Authentication的feature與scenario (屬於物件層級的feature)
  5. 建立Authentication的測試程式
  6. 實作Authentication的Verify方法內容,將頁面的邏輯判斷移到Verify()裡。
  7. 通過Authentication的測試,同時通過Login Feature上的測試。

現在我們已經有Login頁面的驗收測試案例,也有了Authentication物件的測試案例,並且擁有剛好可以通過Login Feature與Authentication Feature的production code。

下一篇文章,則要針對物件層級的integration testing與unit testing,以及運用OOD/OOP的基本原則,來讓整個設計更臻完善,都完成後,就能開始挑選下一個user story。


上一篇
[Day 27]TDD實戰練習-1
下一篇
[Day 29]TDD實戰練習-3
系列文
30天快速上手TDD31

2 則留言

0
ted99tw
iT邦高手 1 級 ‧ 2012-11-05 17:27:49

哭哭哭
來不及偷學呀....
讚讚讚

pajace2001 iT邦研究生 1 級‧ 2012-11-05 19:54:53 檢舉

泰大~你實在謙虛過頭啦落寞

ted99tw iT邦高手 1 級‧ 2012-11-05 20:00:21 檢舉

因為我最近在搞黑莓機的C++開發,離WIN/JAVA是愈來愈遠了呀...Orz

pajace2001 iT邦研究生 1 級‧ 2012-11-05 21:51:01 檢舉

果然是前輩~~~~讚讚讚讚

我要留言

立即登入留言