[30天快速上手TDD][Day 27]TDD 實戰練習 part 1–ATDD 第一個紅燈

[30天快速上手TDD][Day 27]TDD 實戰練習 part 1–ATDD 第一個紅燈

前言

到上一篇文章為止, TDD 所需要的每個片段都已經簡單介紹了一遍,相信各位讀者也很清楚的瞭解,筆者要表達的重點,還是一句話:一切都為了滿足使用者需求

接下來,筆者透過一個簡單的例子,從實作順序面來介紹,怎麼從一個使用者需求開始,到一個循環結束(驗收測試案例可能不夠完整,但循環是一致的)

 

範例介紹

這個例子的背景,是一個網路銀行的系統。

而這邊選用的 user story ,是登入的功能。因為大部分的開發人員做過的系統,應該都有登入的功能,即使沒開發過,至少也使用過。希望透過這樣的例子,讀者會比較好理解,比較有共鳴。

當然因為這不是真實的系統,所以防呆面或需求面可能不夠完善,這邊就先跟讀者們說聲抱歉。

 

定義需求

PO :「嘿,我們的系統,應該要有個登入的功能。當使用者進到系統中,若還沒有經過登入頁面驗證身份的話,要先將使用者導到登入頁。登入成功之後,再導到我們的首頁。」

依照 PO 的說法,與 PO 討論之後,PO、測試人員與開發人員,決定用一個 user story 來描述這樣的一個需求:

「我們需要一個登入的功能:
In order to 驗證身份,避免非法使用者使用系統
As a 線上使用者
I want to 驗證使用者身份是否合法」

如同上一篇文章所介紹的 TDD 開發流程所說,當我們建立了一個 user story 之後,接下來就是:

  1. 依據 user story ,建立一個BDD的 feature 。
  2. 依據 user story break down 為數個驗收測試案例。

 

建立測試專案

首先建立一個測試專案,命名為「TestWebBank」。如下圖所示:

1-create test project

在測試專案中,透過 NuGet 加入幾個在 TDD 中需要用到的參考:

  1. SpecFlow (用來實作 BDD )
  2. Selenium.WebDriver (用來執行 Selenium 測試腳本)
  3. RhinoMocks (用來實作 Unit Testing 中的 Stub 與 Mock object)

並將 SpecFlow 的 App.Config 中的設定,改成使用 MSTest 來執行。如下圖所示:

2-adding reference in testing project

 

建立 Login 的 Feature 檔

在測試專案中,加入一個 Login.Feature 檔。如下圖所示:

3-adding login feature

將 user story 的部分,填入 feature 中,如下圖所示:

4-editing feature

 

確認畫面

確定了 user story 之後,接著測試人員與開發人員,協同 PO 一起討論,該怎麼驗收這個 user story 。

通常 PO 或使用者,需要透過 UI 畫面或雛形系統,才比較容易確認,這樣子是不是他們要的功能。因此,可以透過白板、紙筆、 Word 、 PowerPoint 、建立 prototype/mockup 的工具(例如 Balsamiq MockupsMoqupsaxure)來輔助,迅速地確認這樣的畫面,是否為使用者希望有的功能。

這邊的例子,是開發人員迅速建立一個網站專案,並做了一個只有樣子,但沒有穿衣服的 html 網頁,上面只有兩個輸入項,分別是:

  1. 提款卡 ID
  2. 密碼

以及一個「確認」的登入按鈕。

.aspx程式碼:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        提款卡ID:
        <asp:TextBox ID="txtCardId" runat="server"></asp:TextBox><br />
        密碼:
        <asp:TextBox ID="txtPassword" runat="server" TextMode="Password"></asp:TextBox><br />
        <asp:Button ID="btnLogin" runat="server" Text="確認" />
    </div>
    </form>
</body>
</html>

畫面如下圖所示:

5-login

(開發人員/測試人員:當然畫面不會這麼醜,但基本上是不是畫面只需要有這些輸入項即可?)

確認畫面無誤後,接下來要來定義登入功能中,應該要有的系統行為。

 

建立驗收測試案例

討論後,先定義出登入功能應該要具備下面幾項功能:

  1. 登入成功,導到系統首頁 ( index.aspx )
  2. 登入失敗時,畫面要呈現「驗證失敗,出現密碼錯誤」的訊息

在 user story card 背後,寫上這兩點驗收測試案例之後,接下來我們先在 SpecFlow 的 feature 檔中,將這兩個 scenario 補上去。如下圖所示:

6-adding scenario

筆者建議在描述 scenario 的時候,就應該要有擬真的 input/output 資料,這樣才會比較貼近驗收測試的情況。

而有了這樣的 scenario/acceptance test cases ,也可以方便我們先行準備測試資料。

接著透過 SpecFlow 自動產生 step 的功能,幫我們產生 Login.feature 所對應的 step 檔案內容。如下圖所示:

7-generate step context

Step 的程式碼如下:

using System;
using TechTalk.SpecFlow;

namespace TestWebBank
{
    [Binding]
    public class 登入功能Steps
    {
        [Given(@"在登入頁面")]
        public void Given在登入頁面()
        {
            ScenarioContext.Current.Pending();
        }
        
        [Given(@"提款卡Id輸入""(.*)""")]
        public void Given提款卡Id輸入(int p0)
        {
            ScenarioContext.Current.Pending();
        }
        
        [Given(@"密碼輸入""(.*)""")]
        public void Given密碼輸入(int p0)
        {
            ScenarioContext.Current.Pending();
        }
        
        [When(@"按下確認按鈕")]
        public void When按下確認按鈕()
        {
            ScenarioContext.Current.Pending();
        }
        
        [Then(@"頁面url為""(.*)""")]
        public void Then頁面Url為(string p0)
        {
            ScenarioContext.Current.Pending();
        }
        
        [Then(@"呈現訊息為""(.*)""")]
        public void Then呈現訊息為(string p0)
        {
            ScenarioContext.Current.Pending();
        }
    }
}

到這邊, user story, acceptance test cases, feature, scenario 都定義好了,我們也已經建立好測試專案與網站專案了。

接下來就要開始撰寫驗收測試程式了。

 

撰寫驗收測試程式 – Selenium

我們已經有了簡單的網頁,也有了期望的 scenario ,接下來測試人員就可以開始撰寫自動化的驗收測試程式了。

這邊筆者建議,如果測試人員對 Selenium 的 library 還不夠熟悉時,開發人員可以先給點幫助。例如先 hard-code 寫出兩種結果:

  1. 當按下「確認」按鈕後,導到 index 頁面的功能。
  2. 當按下「確認」按鈕後,出現錯誤訊息的功能。

Hard-code 程式碼如下:

    protected void btnLogin_Click(object sender, EventArgs e)
    {
        //密碼驗證錯誤
        //this.Message.Text = @"密碼輸入錯誤";

        //密碼驗證成功
        //Response.Redirect("index.aspx");
    }

讓測試人員/開發人員,可以先自行透過 Selenium IDE 來錄製 Selenium 腳本。

這邊舉「驗證成功後,要導到 index.aspx 」為例。

  1. 透過 Firefox 瀏覽 Login 頁面。
  2. 打開 Selenium IDE ,開始錄製。
  3. 在提款卡 ID 中,輸入 1234 。
  4. 在密碼中,輸入 91 。
  5. 按下確認按鈕,導到 index.aspx 。

Selenium 錄製腳本,如下圖所示:

錄製輸入資料:

8-record selenium

導到 index 頁面:

9-redirect to index

這邊別忘了,我們還要驗證「是否導到了 index.aspx 」,這裡筆者先加上註解就好,因為最後是要透過 WebDriver 去做驗證。最後將此 scenario 存成 loginSuccess ,當然最好的方式是,存成跟 scenario 可以直接對照的檔名。

10-save selenium test cases

依此類推,將密碼輸入錯誤,驗證失敗的腳本也錄製好。如下圖所示:

11-login failed

 

Export Selenium Test Cases to Selenium.WebDriver Code

如同前面文章: [30天快速上手TDD][Day 8]Integration Testing & Web UI Testing 所提到,我們將錄好的 selenium test cases ,透過 export 轉成 C# with NUnit 的 code 。如下圖所示:

12-save to NUnit

程式碼如下所示:

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.UI;

namespace SeleniumTests
{
    [TestFixture]
    public class LoginSuccess
    {
        private IWebDriver driver;
        private StringBuilder verificationErrors;
        private string baseURL;
        
        [SetUp]
        public void SetupTest()
        {
            driver = new FirefoxDriver();
            baseURL = "http://localhost:10542/";
            verificationErrors = new StringBuilder();
        }
        
        [TearDown]
        public void TeardownTest()
        {
            try
            {
                driver.Quit();
            }
            catch (Exception)
            {
                // Ignore errors if unable to close the browser
            }
            Assert.AreEqual("", verificationErrors.ToString());
        }
        
        [Test]
        public void TheLoginSuccessTest()
        {
            driver.Navigate().GoToUrl(baseURL + "/WebBankSite/Login.aspx");
            driver.FindElement(By.Id("txtCardId")).Clear();
            driver.FindElement(By.Id("txtCardId")).SendKeys("1234");
            driver.FindElement(By.Id("txtPassword")).Clear();
            driver.FindElement(By.Id("txtPassword")).SendKeys("91");
            driver.FindElement(By.Id("btnLogin")).Click();
            // 驗證url是否為index.aspx
        }
        private bool IsElementPresent(By by)
        {
            try
            {
                driver.FindElement(by);
                return true;
            }
            catch (NoSuchElementException)
            {
                return false;
            }
        }
    }
}

有了這樣的 Selenium 自動測試程式,瞭解每一行程式碼的內容之後,我們只需要依照我們所定義好 SpecFlow 的 Scenario ,在 Steps 中,把對應的動作,放進去每一個 scenario 的關鍵字 function 中即可。這邊因為使用的是 MSTest ,因此一些語法也要做點小修改。

修改完成的 step 內容,程式碼如下所示:

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

 

執行 Scenario 的測試

既然測試的程式碼都寫完了,讓我們來執行一下測試,看一下測試的結果。

13-selenium test failed

執行 Selenium WebDriver 的測試時間可能會比較久一點點,因為要透過 WebDriver 啟動 Firefox ,並且執行相關 Selenium test cases 。若讀者需要測試其他瀏覽器,只要參考對應 browser 的 WebDriver 即可。

可以看到兩個測試都失敗了,錯誤訊息分別為:

  1. LoginSuccess: Assert.AreEqual 失敗。預期: <http://localhost:10542/WebBankSite/index.aspx>。實際: http://localhost:10542/WebBankSite/Login.aspx
  2. LoginFailed: Assert.AreEqual 失敗。預期: <密碼輸入錯誤>。實際: <>

紅燈!這就是整個 ATDD 的第一個階段:紅燈

 

小結

為了避免篇幅太長,這篇文章到這邊,就只先介紹了:

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

下一篇文章,就比較單純一點了,我們只要想辦法讓紅燈變成綠燈即可

對敏捷開發有興趣的朋友,可以參考我的粉絲專頁:91敏捷開發之路

對 TDD 課程有興趣的朋友,課程內容、大綱與學員心得,可以參考 skilltree 的公開課程:自動測試與 TDD 實務開發

若需要聯絡我,可以透過粉絲專頁私訊或是側欄的關於我。