[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 之後,接下來就是:
- 依據 user story ,建立一個BDD的 feature 。
- 依據 user story break down 為數個驗收測試案例。
建立測試專案
首先建立一個測試專案,命名為「TestWebBank」。如下圖所示:
在測試專案中,透過 NuGet 加入幾個在 TDD 中需要用到的參考:
- SpecFlow (用來實作 BDD )
- Selenium.WebDriver (用來執行 Selenium 測試腳本)
- 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 Mockups、Moqups、axure)來輔助,迅速地確認這樣的畫面,是否為使用者希望有的功能。
這邊的例子,是開發人員迅速建立一個網站專案,並做了一個只有樣子,但沒有穿衣服的 html 網頁,上面只有兩個輸入項,分別是:
- 提款卡 ID
- 密碼
以及一個「確認」的登入按鈕。
.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>
畫面如下圖所示:
(開發人員/測試人員:當然畫面不會這麼醜,但基本上是不是畫面只需要有這些輸入項即可?)
確認畫面無誤後,接下來要來定義登入功能中,應該要有的系統行為。
建立驗收測試案例
討論後,先定義出登入功能應該要具備下面幾項功能:
- 登入成功,導到系統首頁 ( index.aspx )
- 登入失敗時,畫面要呈現「驗證失敗,出現密碼錯誤」的訊息
在 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 寫出兩種結果:
- 當按下「確認」按鈕後,導到 index 頁面的功能。
- 當按下「確認」按鈕後,出現錯誤訊息的功能。
Hard-code 程式碼如下:
protected void btnLogin_Click(object sender, EventArgs e)
{
//密碼驗證錯誤
//this.Message.Text = @"密碼輸入錯誤";
//密碼驗證成功
//Response.Redirect("index.aspx");
}
讓測試人員/開發人員,可以先自行透過 Selenium IDE 來錄製 Selenium 腳本。
這邊舉「驗證成功後,要導到 index.aspx 」為例。
- 透過 Firefox 瀏覽 Login 頁面。
- 打開 Selenium IDE ,開始錄製。
- 在提款卡 ID 中,輸入 1234 。
- 在密碼中,輸入 91 。
- 按下確認按鈕,導到 index.aspx 。
Selenium 錄製腳本,如下圖所示:
錄製輸入資料:
導到 index 頁面:
這邊別忘了,我們還要驗證「是否導到了 index.aspx 」,這裡筆者先加上註解就好,因為最後是要透過 WebDriver 去做驗證。最後將此 scenario 存成 loginSuccess ,當然最好的方式是,存成跟 scenario 可以直接對照的檔名。
依此類推,將密碼輸入錯誤,驗證失敗的腳本也錄製好。如下圖所示:
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 。如下圖所示:
程式碼如下所示:
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 即可。
可以看到兩個測試都失敗了,錯誤訊息分別為:
- LoginSuccess: Assert.AreEqual 失敗。預期: <http://localhost:10542/WebBankSite/index.aspx>。實際: http://localhost:10542/WebBankSite/Login.aspx。
- LoginFailed: Assert.AreEqual 失敗。預期: <密碼輸入錯誤>。實際: <>。
紅燈!這就是整個 ATDD 的第一個階段:紅燈。
小結
為了避免篇幅太長,這篇文章到這邊,就只先介紹了:
- 如何從 PO 的描述中,定義出 user story 與 acceptance test cases 。
- 如何建立 BDD 相關的 feature 與 scenario 。
- 如何透過 Selenium 來設計驗收測試程式。
- 如何結合 BDD 的 steps 與 Selenium.WebDriver 。
下一篇文章,就比較單純一點了,我們只要想辦法讓紅燈變成綠燈即可。
blog 與課程更新內容,請前往新站位置:http://tdd.best/