iT邦幫忙

DAY 27
4

30天快速上手TDD系列 第 27

[Day 27]TDD實戰練習-1

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

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

上一篇文章:[Day 26]User Story/ATDD/BDD/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」。如下圖所示:

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

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

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

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

將user story的部分,填入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" %>


<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>


    <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>

畫面如下圖所示:

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

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

@建立驗收測試案例
討論後,先定義出登入功能應該要具備下面幾項功能:

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

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

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

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

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

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錄製腳本,如下圖所示:
錄製輸入資料:

導到index頁面:

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

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

@Export Selenium Test Cases to Selenium.WebDriver Code
如同前面文章:[Day 8]Integration Testing & Web UI Testing所提到,我們將錄好的selenium test cases,透過export轉成C# with NUnit的code。如下圖所示:

程式碼如下所示:

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的測試
既然測試的程式碼都寫完了,讓我們來執行一下測試,看一下測試的結果。

(註:執行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。

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


上一篇
[Day 26]User Story/ATDD/BDD/TDD - 總結
下一篇
[Day 28]TDD實戰練習-2
系列文
30天快速上手TDD31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
ted99tw
iT邦高手 1 級 ‧ 2012-11-04 19:21:12

趕快用力抄,用力抄,用力抄啊!!

筆記筆記筆記

我要留言

立即登入留言