[Web Testing][Tool]FluentAutomation (More Behavior-Driven)

[Web Testing][Tool]FluentAutomation (More Behavior-Driven)

前言

之前在介紹 Web Testing 往往都會介紹到 Selenium,有鑑於對許多非測試人員的開發人員來說,因為對 Selenium WebDriver API 並不熟悉,要直接在測試腳本中將 Scenario 轉換成測試程式是有困難的,因此我總是會先介紹 Selenium IDE ,有 IDE 看起來進入門檻就降低不少,加上 Selenium IDE 的 Export 以及 Copy Command 相當簡單好用,所以即使對 API 不熟,也可以透過 Selenium IDE ,甚至 Firefox 上的右鍵清單中直接選取需要的 command ,讓開發人員還是可以簡單的在測試程式中,操作 Selenium 進行自動化的 Web 測試。

對這一段不熟的朋友,請參考:

  1. [30天快速上手TDD][Day 8]Integration Testing & Web UI Testing
  2. [30天快速上手TDD][Day 23]BDD – Introduction
  3. [30天快速上手TDD][Day 24]BDD - SpecFlow Introduction
  4. [30天快速上手TDD][Day 25]BDD - TDD from BDD

然而,即便是透過 Selenium IDE 的輔助,在測試程式中,面對的仍然是 Selenium WebDriver API ,有沒有更簡單的方式,讓開發人員注重的是 Scenario 以及使用者的 Behavior 呢?我在 Scott Hanselman 的一篇推薦 NuGet Package 文章中獲得了解答,請見:NuGet Package of the Week: FluentAutomation for automated testing of Web Applications

FluentAutomation 就是一套把使用者的行為抽象出來的 Web Testing Package ,背後的實作可以選擇 Selenium 或是 WatiN 。而對開發人員來說, Fluent API 的設計,讓開發人員只需著重在 Behavior 。(如果需要著重 Scenario 時,則可以加上 SpecFlow)

這篇文章以之前常介紹的 Login 範例,介紹如何使用 FluentAutomation 來寫測試程式,並與 Selenium WebDriver 做一下比對。

 

LOGIN 範例

Scenario:


Feature: Login
	In order to 避免非法使用者使用系統
	As a 系統管理者
	I want to be 檢查帳號密碼是否合法

Scenario: 登入成功,導到首頁
	Given Login的頁面	
	When 在帳號輸入"joey"
	And 在密碼輸入"1234"	
	And 按下登入
	Then 導到首頁

Scenario: 登入失敗,顯示錯誤訊息
	Given Login的頁面	
	When 在帳號輸入"joey"
	And 在密碼輸入"abc"
	And 按下登入
	Then 顯示"帳號或密碼有誤"

LoginController 程式碼如下:


        [HttpPost]
        public ActionResult Index(string id, string password)
        {
            var isValid = this.authentication.Verify(id, password);

            //if (id == "joey" && password == "1234")
            if (isValid)
            {                
                return RedirectToAction("Index", "Home");
            }
            else
            {
                ViewBag.Message = "帳號或密碼有誤";
                return View();
            }
        }

WEB TESTING BY SELENIUM

登入成功:


using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.UI;

namespace SeleniumTests
{
    [TestClass]
    public class 
    {
        private IWebDriver driver;
        private StringBuilder verificationErrors;
        private string baseURL;
        private bool acceptNextAlert = true;
        
        [TestInitialize]
        public void SetupTest()
        {
            driver = new FirefoxDriver();
            baseURL = "http://localhost:6425/";
            verificationErrors = new StringBuilder();
        }
        
        [TestCleanup]
        public void TeardownTest()
        {
            try
            {
                driver.Quit();
            }
            catch (Exception)
            {
                // Ignore errors if unable to close the browser
            }
            Assert.AreEqual("", verificationErrors.ToString());
        }
        
        [TestMethod]
        public void TheTest()
        {
            driver.Navigate().GoToUrl(baseURL + "/login");
            driver.FindElement(By.Id("id")).Clear();
            driver.FindElement(By.Id("id")).SendKeys("joey");
            driver.FindElement(By.Id("password")).Clear();
            driver.FindElement(By.Id("password")).SendKeys("1234");
            driver.FindElement(By.CssSelector("input[type=\"submit\"]")).Click();
            Assert.AreEqual("http://localhost:6425/", driver.Url);
        }
        private bool IsElementPresent(By by)
        {
            try
            {
                driver.FindElement(by);
                return true;
            }
            catch (NoSuchElementException)
            {
                return false;
            }
        }
        
        private bool IsAlertPresent()
        {
            try
            {
                driver.SwitchTo().Alert();
                return true;
            }
            catch (NoAlertPresentException)
            {
                return false;
            }
        }
        
        private string CloseAlertAndGetItsText() {
            try {
                IAlert alert = driver.SwitchTo().Alert();
                string alertText = alert.Text;
                if (acceptNextAlert) {
                    alert.Accept();
                } else {
                    alert.Dismiss();
                }
                return alertText;
            } finally {
                acceptNextAlert = true;
            }
        }
    }
}

登入失敗:


using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.UI;

namespace SeleniumTests
{
    [TestClass]
    public class 
    {
        private IWebDriver driver;
        private StringBuilder verificationErrors;
        private string baseURL;
        private bool acceptNextAlert = true;
        
        [TestInitialize]
        public void SetupTest()
        {
            driver = new FirefoxDriver();
            baseURL = "http://localhost:6425/";
            verificationErrors = new StringBuilder();
        }
        
        [TestCleanup]
        public void TeardownTest()
        {
            try
            {
                driver.Quit();
            }
            catch (Exception)
            {
                // Ignore errors if unable to close the browser
            }
            Assert.AreEqual("", verificationErrors.ToString());
        }
        
        [TestMethod]
        public void TheTest()
        {
            driver.Navigate().GoToUrl(baseURL + "/login");
            driver.FindElement(By.Id("id")).Clear();
            driver.FindElement(By.Id("id")).SendKeys("joey");
            driver.FindElement(By.Id("password")).Clear();
            driver.FindElement(By.Id("password")).SendKeys("abc");
            driver.FindElement(By.CssSelector("input[type=\"submit\"]")).Click();
            Assert.AreEqual("帳號或密碼有誤", driver.FindElement(By.Id("message")).Text);
        }
        private bool IsElementPresent(By by)
        {
            try
            {
                driver.FindElement(by);
                return true;
            }
            catch (NoSuchElementException)
            {
                return false;
            }
        }
        
        private bool IsAlertPresent()
        {
            try
            {
                driver.SwitchTo().Alert();
                return true;
            }
            catch (NoAlertPresentException)
            {
                return false;
            }
        }
        
        private string CloseAlertAndGetItsText() {
            try {
                IAlert alert = driver.SwitchTo().Alert();
                string alertText = alert.Text;
                if (acceptNextAlert) {
                    alert.Accept();
                } else {
                    alert.Dismiss();
                }
                return alertText;
            } finally {
                acceptNextAlert = true;
            }
        }
    }
}

使用 FLUENTAUTOMATION 設計 WEB TESTING

透過 NuGet 安裝 FluentAutomation,如下:

PM> Install-Package FluentAutomation.SeleniumWebDriver

新增一個測試程式,如下:


        [TestMethod]
        public void TestLoginSuccess_帳號輸入joey_密碼輸入1234_應導到首頁()
        {
            //
            // TODO: Add test logic here
            //
        }

        [TestMethod]
        public void TestLoginFailed_帳號輸入joey_密碼輸入abc_應出現訊息為_帳號或密碼有誤()
        {
            //
            // TODO: Add test logic here
            //
        }

接著把步驟透過 FluentAutomation 的 API 寫上去,如下所示:


    [TestClass]
    public class TestByFluentAutomation : FluentTest
    {
        private string baseUrl = @"http://localhost:6425/";

        public TestByFluentAutomation()
        {
            SeleniumWebDriver.Bootstrap(
                SeleniumWebDriver.Browser.Chrome
                //SeleniumWebDriver.Browser.Firefox,
                //SeleniumWebDriver.Browser.InternetExplorer
            );
            FluentAutomation.FluentSettings.Current.ScreenshotPath = @"C:\LoginCapture";
        }

        [TestMethod]
        public void TestLoginSuccess_帳號輸入joey_密碼輸入1234_應導到首頁()
        {
            I.Open(baseUrl + "login")
                .Enter("joey").In("#id")
                .Enter("1234").In("#password")
                .Click("input[type=\"submit\"]")
                .Assert
                .Url(baseUrl);

            I.TakeScreenshot("login success");
        }

        [TestMethod]
        public void TestLoginFailed_帳號輸入joey_密碼輸入abc_應出現訊息為_帳號或密碼有誤()
        {
            I.Open(baseUrl + "login")
                .Enter("joey").In("#id")
                .Enter("abc").In("#password")
                .Click("input[type=\"submit\"]")
                .Assert
                .Text("帳號或密碼有誤").In("#message");

            I.TakeScreenshot("login failed");
        }
    }

使用方式相當簡單:

  1. 讓測試程式的 Class 繼承自 FluentTest ,即有 I 可以用。
  2. API 都跟講話一樣,在描述使用者的行為,自然可讀性就會高很多。
  3. 尋找 UI 上的 DOM 可以透過 CSS Selector 。

FluentAutomation 還有個很酷的 API ,就是 TakeScreenshot ,可以把當時的頁面擷圖下來,這對自動網頁測試在某些情境下很有幫助,因為自動測試時,並不會有開發人員盯著螢幕看。另外,平行跑多瀏覽器也相當簡單,只需要在 Bootstrap() 中傳入多瀏覽器即可。

上面有提到,要使用 FluentAutomation 的 API ,發動點基本上就是從 I 開始,而這需要將測試的 class 繼承自 FluentTest ,那麼 SpecFlow 中的 Steps 怎麼辦? Steps 並不是實際執行測試的起點啊,答案是沒問題的,繼承 FluentTest 只是一般物件導向的繼承作用,在 SpecFlow 中雖然實際執行測試的起點,是 Feature 檔產生的 .Feature.cs ,但呼叫到的 Steps 方法,就是單純的 Class 與 Function 。

我把原本 Steps 中使用 Selenium WebDriver 的寫法,改成 FluentAutomation ,程式碼如下:


    [Binding]
    [Scope(Feature = "Login")]
    public class LoginSteps : FluentTest
    {
        public LoginSteps()
        {
            SeleniumWebDriver.Bootstrap(
                SeleniumWebDriver.Browser.Chrome
                //SeleniumWebDriver.Browser.Firefox,
                //SeleniumWebDriver.Browser.InternetExplorer
            );
            FluentAutomation.FluentSettings.Current.ScreenshotPath = @"C:\excel";
        }

        //private IWebDriver driver;
        //private StringBuilder verificationErrors;
        private string baseURL;

        //private bool acceptNextAlert = true;

        [BeforeScenario]
        public void BeforeScenario()
        {
            //driver = new FirefoxDriver();
            baseURL = "http://localhost:6425/";
            //verificationErrors = new StringBuilder();
        }

        [AfterScenario]
        public void AfterScenario()
        {
            //try
            //{
            //    driver.Quit();
            //}
            //catch (Exception)
            //{
            //    // Ignore errors if unable to close the browser
            //}
            //Assert.AreEqual("", verificationErrors.ToString());
        }

        [Given(@"Login的頁面")]
        public void GivenLogin的頁面()
        {
            //driver.Navigate().GoToUrl(baseURL + "/login");
            I.Open(baseURL + "/login");
        }

        [When(@"在帳號輸入""(.*)""")]
        public void When在帳號輸入(string id)
        {
            //driver.FindElement(By.Id("id")).Clear();
            //driver.FindElement(By.Id("id")).SendKeys(id);
            I.Enter(id).In("#id");
        }

        [When(@"在密碼輸入""(.*)""")]
        public void When在密碼輸入(string password)
        {
            //driver.FindElement(By.Id("password")).Clear();
            //driver.FindElement(By.Id("password")).SendKeys(password);
            I.Enter(password).In("#password");
        }

        [When(@"按下登入")]
        public void When按下登入()
        {
            //driver.FindElement(By.CssSelector("input[type=\"submit\"]")).Click();
            I.Click("input[type=\"submit\"]");
        }

        [Then(@"顯示""(.*)""")]
        public void Then顯示(string message)
        {
            I.TakeScreenshot("驗證message");
            //Assert.AreEqual(message, driver.FindElement(By.Id("message")).Text);
            I.Assert.Text(message).In("#message");
        }

        [Then(@"導到首頁")]
        public void Then導到首頁()
        {
            //Assert.AreEqual("http://localhost:6425/", driver.Url);
            I.Assert.Url("http://localhost:6425/");
        }
    }

執行結果還是正確的,而且可以看到,寫起來更輕鬆了,比起 SeleniumWebDriver API 更加直覺與好懂。

 

結論

一個程式設計師的功力,除了產品達到使用者的需求以外,還考驗著程式設計師的抽象設計能力。

測試程式與產品程式碼是一體兩面的,產品程式碼要好懂、好維護、沒有壞味道,測試程式也是如此。因此, FluentAutomation 很漂亮的把網頁的測試抽象為使用者的行為,透過 Fluent API 的設計,讓測試程式更像使用者操作的動線,自然就更貼近或更能表現出 Scenario 的味道與畫面。

我自己的感想是,發現 FluentAutomation 真的是如獲至寶,上手簡單,又沒什麼額外的副作用,又能解決過去碰到的諸多問題。我敢肯定之後的 Web 測試,我大概首選都是 FluentAutomation 。

 

Reference

  1. Scott Hanselman的簡介文章:http://www.hanselman.com/blog/NuGetPackageOfTheWeekFluentAutomationForAutomatedTestingOfWebApplications.aspx
  2. 官方網站:http://fluent.stirno.com/
  3. Github:https://github.com/stirno/FluentAutomation
  4. Documentation:http://fluent.stirno.com/docs/

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

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

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