2017-06-08

使用 Fluent Automation 與 Selenium 打造語意化 Web UI 測試程式

之前文章 使用 Selenium IDE 與 C# 做 Web UI 測試,介紹了如何使用 Selenium IDE 錄製網頁操作並將過程 export 成 c# 測試程式,雖然很方便但 Selenium api 太像程式碼,一般而言測試案例是用來描述需求,這樣會讓測試案例的可讀性降低,一旦測試的可讀性降低,漸漸的就會造成維護不易,時間一久就愈來愈少人願意維護而造成 code coverage 不增反降,這對於程式碼品質絕對有負面影響。

今天來紀錄一下 91 哥在 twMVC 活動中介紹的 Fluent Automation 套件,看它可以如何讓我們的 Web UI 測試有更好的可讀性


基本設定

案例會持續延用 使用 Selenium IDE 與 C# 做 Web UI 測試,錄製登入 GitHub

  • 主要測試程式如下(如果你懶得錄,可以使用下面的版本,但請記得改帳號跟密碼)
    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;
    using FluentAutomation;
    
    namespace SeleniumTests
    {
        [TestFixture]
        public class Ngithub
        {
            private IWebDriver driver;
            private StringBuilder verificationErrors;
            private string baseURL;
            private bool acceptNextAlert = true;
    
            [SetUp]
            public void SetupTest()
            {
                driver = new FirefoxDriver();
                baseURL = "https://github.com/";
                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 TheNgithubTest()
            {
                driver.Navigate().GoToUrl(baseURL + "/login");
                driver.FindElement(By.Id("login_field")).Clear();
                driver.FindElement(By.Id("login_field")).SendKeys("{帳號}");
                driver.FindElement(By.Id("password")).Clear();
                driver.FindElement(By.Id("password")).SendKeys("{密碼}");
                driver.FindElement(By.Name("commit")).Click();
                // 這邊要稍等一下,讓 github 完成 redirect
                Thread.Sleep(1000);
                //驗證網址是否正確
                Assert.AreEqual(baseURL, 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;
                }
            }
        }
    }
    
    

安裝 NuGet 套件

  1. FluentAutomation.SeleniumWebDriver
    • 專案上按右鍵 --> Manage NuGet Packages

      1nuget

    • Browse --> 搜尋 FluentAutomation.SeleniumWebDriver --> Install

      2install

    • 會一併安裝其他相關套件

      3otherpacks

  2. Selenium.WebDriver.ChromeDriver
    • 專案上按右鍵 --> Manage NuGet Packages

      1nuget

    • Browse --> 搜尋 Selenium.WebDriver.ChromeDriver --> Install

      4install

    • 這邊留意一下套件名稱,名稱很容易混洧

      5mass

    • 這個套件我試了好久才試出來,如果沒裝會出現下列錯誤
      Test Name: Test1
      Test FullName: TestVSIXN3Tests.SampleTests.Test1
      Test Source: C:\Users\yowko.tsai\Documents\Visual Studio 2017\Projects\TestVSIX\TestVSIXN3Tests\SampleTests.cs : line 24
      Test Outcome: Failed
      Test Duration: 0:00:01.732
      
      Result StackTrace: 
      at FluentAutomation.BaseCommandProvider.<>c__DisplayClass9.<WaitUntil>b__8()
         at FluentAutomation.BaseCommandProvider.Act(CommandType commandType, Action action)
         at FluentAutomation.BaseCommandProvider.WaitUntil(Expression`1 conditionAction, TimeSpan timeout)
         at FluentAutomation.BaseCommandProvider.Act(CommandType commandType, Action action)
         at FluentAutomation.CommandProvider.Navigate(Uri url)
         at FluentAutomation.ActionSyntaxProvider.Open(Uri url)
         at FluentAutomation.ActionSyntaxProvider.Open(String url)
         at TestVSIXN3Tests.SampleTests.Test1() in C:\Users\yowko.tsai\Documents\Visual Studio 2017\Projects\TestVSIX\TestVSIXN3Tests\SampleTests.cs:line 25
      --InvalidOperationException
         at OpenQA.Selenium.Remote.RemoteWebDriver.UnpackAndThrowOnError(Response errorResponse)
         at OpenQA.Selenium.Remote.RemoteWebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
         at OpenQA.Selenium.Remote.RemoteCookieJar.DeleteAllCookies()
         at FluentAutomation.CommandProvider.<>c__DisplayClass2.<.ctor>b__0()
         at System.Lazy`1.CreateValue()
         at System.Lazy`1.LazyInitValue()
         at System.Lazy`1.get_Value()
         at FluentAutomation.CommandProvider.get_webDriver()
         at FluentAutomation.CommandProvider.<>c__DisplayClass5.<Navigate>b__4()
         at lambda_method(Closure )
         at FluentAutomation.BaseCommandProvider.<>c__DisplayClass9.<WaitUntil>b__8()
      Result Message: 
      FluentAutomation.Exceptions.FluentException : An unexpected exception was thrown inside WaitUntil(Action). See InnerException for details.
        ----> System.InvalidOperationException : unknown error: Runtime.executionContextCreated has invalid 'context': {"auxData":{"frameId":"9064.1","isDefault":true},"id":1,"name":"","origin":"://"}
        (Session info: chrome=58.0.3029.110)
        (Driver info: chromedriver=2.9.248315,platform=Windows NT 6.1 SP1 x86_64)
      

      6error

如何使用

  1. 引用 FluentAutomation

    using FluentAutomation;
    
  2. 測試 class 繼承 FluentTest

    public class Ngithub : FluentTest
    
  3. 建構式中指定使用的瀏覽器

    SeleniumWebDriver.Bootstrap(
               SeleniumWebDriver.Browser.Firefox
           );
    

    可指定多個瀏覽器

  4. 使用語意化語法

    I.Open(baseURL+"login")
        .Enter("{帳號}").In("#login_field")
        .Enter("{密碼}").In("#password")
        .Click("input[name='commit']")
        .Wait(1)
        .Assert.Url(baseURL);
    
  5. 完整程式

    using NUnit.Framework;
    using FluentAutomation;
    
    namespace SeleniumTests
    {
        [TestFixture]
        public class Ngithub : FluentTest
        {
            private string baseURL;
            public Ngithub()
            {
                baseURL = "https://github.com/";
                SeleniumWebDriver.Bootstrap(
                   SeleniumWebDriver.Browser.Firefox
               );
            }
    
            [Test]
            public void TheNgithubTest()
            {
    
                I.Open(baseURL+"login")
                    .Enter("{帳號}}").In("#login_field")
                    .Enter("{密碼}").In("#password")
                    .Click("input[name='commit']")
                    .Wait(1)
                    .Assert.Url(baseURL);
            }
        }
    }
    

其他驗證方式

  • 驗證在 selector 中是否有特定 css class
    I.Assert.Class("{class name}").Of("{selector}");
    
  • 驗證特定的 selector 數量
    I.Assert.Count(1).Of("{selector}");
    
  • 驗證頁面上是否有特定 selector
    I.Assert.Exists("{selector}");
    
  • 驗證匿名函式結果是否為 false
    var element = I.Find("input");
    I.Assert.False(() => element().IsSelect);
    
  • 驗證 selector 內容是否為指定文字
    • 可以用於有 innerHTML 或是可以提供 文字 值的 DOM element 上
      I.Assert.Text("FluentAutomation").In("{selector}");
      
    • 可以用來驗證回傳 true|false 的匿名函式
      I.Assert.Text((text) => text.Length > 50).In("{selector}");
      
  • 驗證匿名函數應該出現 exception

    用來做反向驗證 --> 驗證某個 selector 不該存在

    //登入成功沒有這個 div 會 pass ,登入失敗出現這個 div 會 fail
    I.Assert.Throws(() => I.Assert.Exists("#js-flash-container .flash.flash-full.flash-error .container"));
    
  • 驗證 value 是否合乎預期
    • 可以使用 selector 選定的 <INPUT><TEXTAREA><SELECT>
      I.Assert.Value(10).In("{selector}");
      
    • 可以用來驗證回傳 true|false 的匿名函式
      I.Assert.Value((value) => value.StartsWith("M")).In("{selector}");
      
  • 驗證匿名函數結果應該為 true
    var element = I.Find("select");
    I.Assert.True(() => element().IsSelect);
    
  • 驗證瀏覽器是否有 url
    • 可以驗證 string 或是 Uri
      I.Assert.Url(url);
      
    • 可以用來驗證回傳 true|false 的匿名函式
      I.Assert.Url((uri) => uri.Scheme == "https");
      

心得

Fluent Automation 從 2015 至今都沒有更新紀錄,而且連 Fluent Automation 的官網已經失效了:http://fluent.stirno.com,幸好 GitHub 上還有相關資訊,官方文件比較不好找 文件可以參考 FluentAutomation Docs,雖然使用上便利性稍差但功能本身非常好用,前後比較一下使用 Fluent Automation 是不是覺得意圖一目瞭然,可讀性超高呀?!

參考資訊

  1. 使用 Selenium IDE 與 C# 做 Web UI 測試
  2. FluentAutomation Docs
  3. GitHub - FluentAutomation

沒有留言:

張貼留言