無論你是用哪一種自動化測試的驅動框架,當我們構建一個復雜應用程序的自動化測試的時候。都希望構建一個測試流程穩定,維護成本較低的自動化測試。但是,現實往往沒有理想豐滿。而這一篇,我會為大家講解我們在使用Selenium進行Web測試的時候應該如何控制我們的測試流程,從而盡可能地提高自動化測試可維護性。那么,先看一下這一篇的內容主要涉及到的話題:
- 自動化測試的成本
- 隱式的等待同步策略
- 顯式的等待同步策略
- 自定義等待同步策略(一些關於自動化框架設計的探討)
(一)自動化測試的成本
《Selenium For C#》的系列文章寫到這里,我覺得是時候跟大家分享一下關於自動化測試維護成本的問題了。對於自動化測試帶來的好處我就不多說了,網上對它推崇之詞猶如滔滔江水連綿不絕。這里我想提一下構建一個自動化測試的成本是什么?如果你的公司或是團隊想要為自己的產品構建完整的自動化測試。那么,我接下來要描述的這些都將會是你需要付出的成本。當然,與此同時你也會得到自動化測試帶來的好處。
- 人員成本:自動化測試的構建,勢必需要一些技術要求較高的架構人員來完成。對於框架的穩定性、易用性、安全、集成部署... ...等因素的要求,都是遠高於開發框架的。
- 開發成本:這一部分的成本,往往是最明顯的。自動化測試的開發量幾乎等同於待測試系統的開發量。
- 維護成本:自動化構建的所有成本中,維護成本往往是最難預測的,過高的維護成本會直接導致自動化構建的失敗。
我見過一些公司構建的自動化測試,每一輪運行會花費10個小時左右的時間。會有大量的case失敗,但其中大部分都是Case本身的原因而非Bug引起的。這就直接導致了QA需要花費大量的時間在調試失敗的Case上。此時,我們再想想當初構建自動化測試的初衷,是為了節約人力成本,讓人力成本可以花在更加有效的地方。但上述這樣的自動化構建真是實現了我們最初的目的嗎?所以,你所構建的自動化測試平台的質量直接關系到此次構建的成敗。關於這個話題也不是一兩句能說明白的,如果有時間,我會寫一個關於自動化測試平台構建的系列,羅列一下自認為較好的自動化測試的具體實踐。在此,只是先拋出我個人的一個觀點:“如果你的自動化測試維護成本遠遠高於手動測試的成本。那么,我想這樣的自動化測試構建是需要慎重考量的,或者說是失敗的!”。
(二)等待同步策略
本系列的文章雖然只是對Selenium技術做一些講解,但是等待同步策略是每一個自動化框架的設計人員都應該了解的。而且在具體的實踐中,由於等待同步策略的使用不當而導致Case失敗的例子也不在少數。
在實際測試運行時,測試可能並不是以相同的速度響應。例如,一個進度條會等待幾秒才會100%,頁面上的某個圖表需要加載一段時間才會顯示出來。隨着Web技術的發展,更多的延遲加載、Ajax以及RealTime技術的廣泛使用。對自動化測試Case提出了更高的要求。對於接觸過自動化測試,或是參與過自動化測試實踐的同學應該會有這樣經驗:很多自動化測試的用例在調試的時候沒有問題,但是一旦在CI集成系統中運行就會出現元素找不到的異常(多數是因為元素還沒有來得及加載)。遇見這種情況,多數會把問題歸結於環境的影響。但是,也有可能是測試用例本身不夠健壯。下面我來給大家介紹一下Selenium框架為我們提供的等待機制,希望對各位小伙伴書寫健壯的測試用例提供一些幫助。
(三)隱式的等待同步策略
Selenium WebDriver提供了隱式的等待策略,我們可以設置一個超時時間。如果WebDriver沒有在DOM中找到元素(不會馬上拋出異常),它將繼續等待直到超過了設定的隱式等待時間,才會拋出找不到元素的異常,隱式的等待同步設置很簡單,具體的設置方式如下所示:
1 /// <summary> 2 /// demo1 : 設置等待同步策略 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.TestFlowControl.Demo1")] 5 public void TestFlowControl_Demo1() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 // 1. 隱式的等待 同步測試 9 driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(10)); 10 11 driver.Close(); 12 }
WebDriver的Manage().Timeouts() 返回的是一個實現了ITimeouts接口的對象。這里我們可以順便了解一下該接口定義的其他方法:
- ImplicitlyWait:設置默認的等待時間。
- SetPageLoadTimeout:設置默認的頁面加載超時時間。
- SetScriptTimeout:設置JavaScrip執行超時時間。
1 // Summary: 2 // Defines the interface through which the user can define timeouts. 3 public interface ITimeouts 4 { 5 6 ITimeouts ImplicitlyWait(TimeSpan timeToWait); 7 8 ITimeouts SetPageLoadTimeout(TimeSpan timeToWait); 9 10 ITimeouts SetScriptTimeout(TimeSpan timeToWait); 11 }
因此我們可以更細粒度的測試程序的加載時間(雖然這些是性能測試應當關注的問題),可以看到ITimeouts的接口定義的方法也都是自引用的(關於自引用可以參照《Lesson 05 - Selenium For C# 之 API 下》),所以我們可以用這樣的方式來調用:
1 /// <summary> 2 /// demo1 : 設置等待同步策略 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.TestFlowControl.Demo1")] 5 public void TestFlowControl_Demo1() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 // 1. 隱式的等待 同步測試 9 driver.Manage().Timeouts() 10 .ImplicitlyWait(TimeSpan.FromSeconds(10)) 11 .SetPageLoadTimeout(TimeSpan.FromSeconds(10)) 12 .SetScriptTimeout(TimeSpan.FromSeconds(10)); 13 14 driver.Close(); 15 }
(四)顯示的等待同步策略
設置隱式的等待可以解決元素加載時間導致DOM元素定位失敗的問題。但隱式的等待是全局屬性,一旦設置了隱式的等待就會影響整個運行計划的時間。所以,更推薦的方式是顯式的等待同步策略。盡量的使用顯式的等待,Selenium Webdriver也為我們提供了許多更加精准的定位方式,先看一個Demo:
1 /// <summary> 2 /// demo2 :設置顯示等待同步策略 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.TestFlowControl.Demo2")] 5 public void TestFlowControl_Demo2() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 //省略操作代碼.... 9 WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); 10 wait.Until(ExpectedConditions.ElementExists(By.Id("Object ID"))); 11 }
上述代碼是一個顯示等待的例子,Selenium WebDriver 提供了WebDriverWait的控制等待相關配置(這里我們設置了等待時間),而后我們使用了對象的Until方法等待,直到某個元素存在為止。Unitl方法的定義如下,他接受一個Func<T,TResult>類型的參數,Func類型是C#3.5之后引入的針對委托類型的簡化定義。在此,你可以把它理解為需要傳遞一個操作方法。而Unitl方法會循環的執行用戶傳入的方法(這里就是等待元素出現)。直到超時或者操作方法滿足下列條件之一:
- 函數返回值不為 null。
- 函數返回值不為 flase。
- 函數拋出的異常不再設置的 ignored exception列表中。(可以用 wait.IgnoreExceptionTypes設置需要忽略的異常)
1 public TResult Until<TResult>(Func<T, TResult> condition);
通常情況下,一般的編寫自動化測試Case的人員是不需要直接編寫Until的Func參數的執行操作,這部分內容通常是由框架開發人員完成。另外,Selenium WebDriver 已經為我們提供了常見的操作函數,也就是上面Code中用到的ExpectedConditions類,它內部已經集成了大量的方法供框架消費者使用。這些方法的功能已經在命名上有了很好的體現,這里就不一一介紹,我簡單的列舉一下當前使用的Selenium WebDriver(2.49.0)的版本ExpectedConditions包含方法:
1 // Summary: 2 // Supplies a set of common conditions that can be waited for using OpenQA.Selenium.Support.UI.WebDriverWait. 3 public sealed class ExpectedConditions 4 { 5 public static Func< IWebDriver, IAlert > AlertIsPresent(); 6 public static Func< IWebDriver, bool > AlertState(bool state); 7 public static Func< IWebDriver, IWebElement > ElementExists(By locator); 8 public static Func< IWebDriver, IWebElement > ElementIsVisible(By locator); 9 public static Func< IWebDriver, bool > ElementSelectionStateToBe(By locator, bool selected); 10 public static Func< IWebDriver, bool > ElementSelectionStateToBe(IWebElement element, bool selected); 11 public static Func< IWebDriver, IWebElement > ElementToBeClickable(By locator); 12 public static Func< IWebDriver, IWebElement > ElementToBeClickable(IWebElement element); 13 public static Func< IWebDriver, bool > ElementToBeSelected(By locator); 14 public static Func<IWebDriver, bool> ElementToBeSelected(IWebElement element); 15 public static Func< IWebDriver, bool > ElementToBeSelected(IWebElement element, bool selected); 16 public static Func< IWebDriver, IWebDriver > FrameToBeAvailableAndSwitchToIt(By locator); 17 public static Func< IWebDriver, IWebDriver > FrameToBeAvailableAndSwitchToIt(string frameLocator); 18 public static Func< IWebDriver, bool > InvisibilityOfElementLocated(By locator); 19 public static Func< IWebDriver, bool > InvisibilityOfElementWithText(By locator, string text); 20 public static Func< IWebDriver, ReadOnlyCollection <IWebElement >> PresenceOfAllElementsLocatedBy(By locator); 21 public static Func< IWebDriver, bool > StalenessOf(IWebElement element); 22 public static Func< IWebDriver, bool > TextToBePresentInElement(IWebElement element, string text); 23 public static Func< IWebDriver, bool > TextToBePresentInElementLocated(By locator, string text); 24 public static Func< IWebDriver, bool > TextToBePresentInElementValue(By locator, string text); 25 public static Func< IWebDriver, bool > TextToBePresentInElementValue(IWebElement element, string text); 26 public static Func< IWebDriver, bool > TitleContains(string title); 27 public static Func< IWebDriver, bool > TitleIs(string title); 28 public static Func< IWebDriver, bool > UrlContains(string fraction); 29 public static Func< IWebDriver, bool > UrlMatches(string regex); 30 public static Func< IWebDriver, bool > UrlToBe(string url); 31 public static Func< IWebDriver, ReadOnlyCollection <IWebElement >> VisibilityOfAllElementsLocatedBy(By locator); 32 public static Func< IWebDriver, ReadOnlyCollection <IWebElement >> VisibilityOfAllElementsLocatedBy(ReadOnlyCollection <IWebElement > elements); 33 }
(五)自定義等待同步策略
最后這一部分,其實是大多自動化測試的使用人員不會用到的部分。但對於構建測試框架的架構師而言,這一部分則是必不可少的。實際的應用場景中總會有針對自己程序特定的等待需求。比如:待測試的應用程序在所有的頁面異步加載的時候,都會出現一個等待的進度條,也就是說我們的操作需要等待這個進度條消失之后才能進行。那么,對於這部分功能的處理就不應該有上層的測試框架使用人員來完成,框架開發人員應當開發一個簡單的接口供上層測試人員使用。關於接口的開發規范,開發方式的討論已經超出了本系列的范圍,這里我只是簡單的展示一下如何實現功能,在測試框架開發的相關文章中,我會詳細的描述這部分的內容。
首先,我們看看如何做一個簡單的擴展,剛剛提到了,Func是一個委托的簡化定義,so... ... 我們可以用如下方式來擴展:
1 /// <summary> 2 /// demo3 : 擴展等待 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.SeleniumAPI.Demo3")] 5 public void SeleniumAPI_Demo3() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 //省略操作代碼.... 9 WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); 10 wait.Until<bool>( 11 delegate(IWebDriver dir) 12 { 13 var element = dir.FindElement(By.XPath(".//div[@id='divProcessBar']")); 14 return !element.Displayed; 15 } 16 ); 17 }
上面的Code,定義了匿名委托實現了之前描述的針對進度條消失的等待。但是這樣的實現方式是需要所有的測試框架使用這都了解委托的概念的,一般來說測試用例編寫人員都是來自QA Team的,這樣的要求會直接提高對框架使用者的技術要求,同樣這樣的寫法也違背了很多面向對象程序設計的基本原則。更推薦的寫法是單獨定義一個靜態類(這個靜態類由框架開發人員維護):
1 /// <summary> 2 /// 自定義的擴展條件 3 /// </summary> 4 public class ExpectedConditionsExtension 5 { 6 /// <summary> 7 /// 等待進度條消失 8 /// </summary> 9 /// <param name="dir">WebDriver對象</param> 10 /// <returns>操作Func對象</returns> 11 public static Func<IWebDriver, bool> ProcessBarDisappears() 12 { 13 return delegate(IWebDriver driver) 14 { 15 IWebElement element = null; 16 try 17 { 18 element = driver.FindElement(By.XPath(".//div[@id='divProcessBar']")); 19 } 20 catch (NoSuchElementException) { return true; } 21 return !element.Displayed; 22 }; 23 } 24 }
現在對於之前上層消費代碼的調用就變成了下面的樣子:
1 /// <summary> 2 /// demo4 : 擴展等待 升級版 3 /// </summary> 4 [Fact(DisplayName = "Cnblogs.TestFlowControl.Demo4")] 5 public void TestFlowControl_Demo4() 6 { 7 IWebDriver driver = new FirefoxDriver(); 8 //省略操作代碼.... 9 WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); 10 wait.Until(ExpectedConditionsExtension.ProcessBarDisappears()); 11 }
需要說明的是,這不是簡單的語法糖(即僅僅簡化代碼),它帶來更多的好處是降低了對框架的使用人員的技術要求。由於這里我們不需要使用框架的人懂得C#的委托(當然懂了更好),所以就降低了框架入門的成本,試想一下如果你是一個測試框架的架構師。你的用戶就是你們團隊的QA。面對上述的兩種方式,我相信后一種調用方式會更容易讓他們接受和正確的使用。另一方面,由於做了這樣的封裝,如果開發人員修改了進度條的結構我們的可控的(只需修改擴展類中的定位)。當然,你也可以要求所有的QA熟練的掌握C#的各種高級語法知識。但如果是那樣的話,我們開發測試框架的意義何在?開發測試框架的一個重要目的不就是為了屏蔽復雜的技術實現,降低學習成本嗎? 這個思想,也將會是后續關於測試框架設計的主導思想(畢竟測試框架的用戶是QA,請不要默認他們具有高級開發級別的能力,這里絕沒有說QA的能力不如開發,我只是想說明從設計一個測試框架的角度來講易用性是一個很重要的指標)。
《Selenium For C#》的相關文章:Click here.
- [小北De編程手記] : Lesson 01 - Selenium For C# 之 環境搭建
- [小北De編程手記] : Lesson 02 - Selenium For C# 之 核心對象
- [小北De編程手記] : Lesson 03 - Selenium For C# 之 元素定位
- [小北De編程手記] : Lesson 04 - Selenium For C# 之 API 上
- [小北De編程手記] : Lesson 05 - Selenium For C# 之 API 下
- [小北De編程手記] : Lesson 06 - Selenium For C# 之 流程控制
- [小北De編程手記] : Lesson 07 - Selenium For C# 之 窗口處理
- [小北De編程手記] : Lesson 08 - Selenium For C# 之 PageFactory & 團隊構建
說明:Demo地址:https://github.com/DemoCnblogs/Selenium