在Visual Studio中,Coded UI Test已經不是什麼新特性了,較早版本的Visual Studio中就已經有這個東東了。它主要用來幫助自動化測試工程師和開發人員確保程序在UI方面沒有任何問題。這其中包含了豐富的內容。在這以前,我一直對自動化測試的工做以及什麼是自動化測試只知其一;不知其二,具有自動化測試編碼能力的工程師所掌握的技能在某種程度上要遠超程序開發人員和設計人員,對於這一點,我早有耳聞!但直到親身體驗我才確信,測試工做遠沒有咱們想象得那麼簡單。開發人員或許花上數小時就能夠完成項目中某一個獨立模塊並使其在必定範圍內正常運行,然而,自動化測試工程師也許會花上好幾天的時間來編寫對應的自動化測試代碼來確保這一功能運行正常。web
Coded UI Test包含了十分豐富的API庫,它能夠錄製和回放UI操做,捕捉UI元素並獲取屬性的值,並生成操做代碼。測試人員在生成代碼的基礎上對測試對象的值進行邏輯判斷並給出測試結果。建立一個Coded UI Test很容易,大多數狀況下,咱們只須要藉助於Visual Studio就能夠完成絕大部分操做。爲了說明整個操做過程,咱們假設一個測試需求:設計模式
在瀏覽器中打開百度搜索,輸入「jaxu cnblogs」關鍵字,搜索並查看結果的第一條是否爲「Jaxu - 博客園」
基本操做瀏覽器
(本文演示的全部代碼和操做均在Visual Sutdio 2013和Windows 8.1 + IE 11環境下)服務器
在Visual Studio中開始建立一個Coded UI Test Project。這很簡單!app
工程建立成功後,Visual Studio會問你是立刻開始一個新的UI錄製仍是選擇已經錄製好的操做。固然你也能夠選擇取消,在後面的步驟裏再開始UI錄製。函數
工程默認生成CodedUITest1.cs文件。在開始錄製UI操做以前,對基本概念作一下介紹:測試
而後,咱們開始一個UI錄製。在工程中添加一個Coded UI Test Map文件。建立成功後Visual Studio會自動在屏幕的右下角打開Coded UI Test Builder窗口,以方便咱們進行UI錄製操做。ui
借用MSDN上的圖片來對Coded UI Test Builder窗口上按鈕的功能作一下簡單的說明:this
UI Action的錄製和UI控件的選擇操做是分開的。讓咱們先開始UI Action的錄製。搜索引擎
在Solution Explorer中展開UIMap1.uitest文件,選擇並打開UIMap1.Designer.cs文件,能夠看到剛纔所生成的代碼。是否是很想如今就運行一下,來看看這些自動生成的代碼如何運行?如今還不行,由於單純的UI Action運行沒有任何意義,Coded UI Test的真正意義是經過UI操做來定位到UI上的某一個特定元素,並最終經過斷言來肯定該元素的屬性是否和預期的值相等。
爲了可以手動修改.Designder.cs文件中生成的代碼,咱們須要將它們移到.cs文件中。在Solution Explorer中雙擊UIMap1.uitest文件,在打開的窗口中咱們能夠看到左邊是UI Actions所生成的步驟,右邊是UI Control Map(稍後咱們會用到它)。在左邊的UI Actions中選擇根節點RecordedMethod1,而後在頂部的菜單中選擇Move code to UIMap1.cs,代碼會被移到.cs文件以方便咱們進行修改。完成該步驟以後,咱們能夠在.cs文件中看到這些代碼並作相應的修改。
你可能已經注意到了,自動生成的代碼中有些對象的名字看起來並不那麼好,甚至有些還包含了中文。你但願修改它們,可是不要在.Designer.cs文件中作任何修改!還記得前面咱們講過的Edit With Coded UI Test操做嗎?在Solution Explorer中右鍵選擇UIMap1.uitest文件,右鍵選擇Edit With Coded UI Test打開Coded UI Test Builder窗口,而後點擊Add assertions按鈕(就是那個用來選擇UI Control的按鈕),而後展開UI Control Map界面。以下圖,咱們能夠對其中生成的UI Controls進行編輯和重命名。
完成修改以後再次點擊Generate code按鈕並關閉Coded UI Test Builder窗口,此時.Designer.cs文件中自動生成的代碼已經作了修改。因爲前面咱們已經將相關的UI Actions部分的代碼移到.cs文件裏了,因此重命名的對象咱們還須要在.cs文件中手動進行修改,不然編譯時會出錯。建議在將代碼移到.cs文件以前完成自動生成代碼的修改工做,以免手動修改過多的代碼。
而後咱們須要捕捉到百度搜索結果的UI控件,並對其中的結果進行判斷。仍然使用Coded UI Test Builder窗口。
至此,全部的UI Actions和UI Controls都已經定義完畢,接下來咱們要編碼以完成對搜索結果的判斷。藉助於自動生成的代碼,咱們編寫了下面的測試方法以實現文章最開始的測試需求。
namespace CodedUITestProject2.UIMap1Classes { using Microsoft.VisualStudio.TestTools.UITesting.HtmlControls; using Microsoft.VisualStudio.TestTools.UITesting.WinControls; using System; using System.Collections.Generic; using System.CodeDom.Compiler; using Microsoft.VisualStudio.TestTools.UITest.Extension; using Microsoft.VisualStudio.TestTools.UITesting; using Microsoft.VisualStudio.TestTools.UnitTesting; using Keyboard = Microsoft.VisualStudio.TestTools.UITesting.Keyboard; using Mouse = Microsoft.VisualStudio.TestTools.UITesting.Mouse; using MouseButtons = System.Windows.Forms.MouseButtons; using System.Drawing; using System.Windows.Input; using System.Text.RegularExpressions; public partial class UIMap1 { public void TestSearchResult() { HtmlDiv resultPanel = this.UINewtabInternetExplorWindow.UIJaxucnblogs_SearchDocument.UIContent_leftPane; HtmlDiv resultPanelFirst = (HtmlDiv)resultPanel.GetChildren()[0]; HtmlHyperlink link = new HtmlHyperlink(resultPanelFirst); Assert.AreEqual("Jaxu - 博客園", link.InnerText, "Validation is failed."); } /// <summary> /// RecordedMethod1 - Use 'RecordedMethod1Params' to pass parameters into this method. /// </summary> public void RecordedMethod1() { #region Variable Declarations WinEdit uIItemEdit = this.UINewtabInternetExplorWindow.UIItemWindow.UIItemEdit; HtmlEdit uIWDEdit = this.UINewtabInternetExplorWindow.UIDocument.UIWDEdit; HtmlInputButton uISearchButton = this.UINewtabInternetExplorWindow.UIDocument.UISearchButton; #endregion // Go to web page 'about:Tabs' using new browser instance this.UINewtabInternetExplorWindow.LaunchUrl(new Uri("http://www.baidu.com")); // Type 'www.baidu{Enter}' in text box //Keyboard.SendKeys(uIItemEdit, this.RecordedMethod1Params.UIItemEditSendKeys, ModifierKeys.None); // Type 'jaxu cnblogs' in 'wd' text box uIWDEdit.Text = this.RecordedMethod1Params.UIWDEditText; // Click '百度一下' button Mouse.Click(uISearchButton, new Point(61, 18)); } public virtual RecordedMethod1Params RecordedMethod1Params { get { if ((this.mRecordedMethod1Params == null)) { this.mRecordedMethod1Params = new RecordedMethod1Params(); } return this.mRecordedMethod1Params; } } private RecordedMethod1Params mRecordedMethod1Params; } /// <summary> /// Parameters to be passed into 'RecordedMethod1' /// </summary> [GeneratedCode("Coded UITest Builder", "12.0.21005.1")] public class RecordedMethod1Params { #region Fields /// <summary> /// Go to web page 'about:Tabs' using new browser instance /// </summary> public string UINewtabInternetExplorWindowUrl = "about:Tabs"; /// <summary> /// Type 'www.baidu{Enter}' in text box /// </summary> public string UIItemEditSendKeys = "www.baidu{Enter}"; /// <summary> /// Type 'jaxu cnblogs' in 'wd' text box /// </summary> public string UIWDEditText = "jaxu cnblogs"; #endregion } }
大部分代碼是由Coded UI Test Builder自動生成的,咱們只編寫了TestSearchResult()方法,用來尋找控件並獲取到其中的值來進行判斷。測試結果的判斷經過Assert斷言來完成,Assert提供了多種方法以幫助咱們實現不一樣的判斷,具體的內容能夠參考msdn。而後對RecordedMethod1()方法作了適當修改。TestSearchResult()方法中對於如何查找和遍歷UI控件在稍後的章節中會討論到。而後咱們將全部代碼的調用放到CodedUITest1.cs文件中執行。
[TestMethod] public void CodedUITestMethod1() { UIMap1 uimap = new UIMap1(); uimap.RecordedMethod1(); uimap.TestSearchResult(); }
如今能夠經過Test Explorer窗口或者直接使用測試方法的上下文菜單運行或調試該測試方法。若是經過測試,測試方法前面會顯示綠色的圖標,不然會顯示紅色的叉。Visual Studio會爲每次測試生成對應的測試報告,在工程目錄下的TestResults文件夾中能夠找到全部的測試報告。
有關Assert斷言
在自動化測試中,Assert斷言一旦遇到測試失敗的狀況就會拋出異常,從而致使接下來的測試方法或任務不會繼續執行。也就是說,若是一個測試工程中包含了諸多測試方法,常常的狀況是一個測試工程中會包含不少個測試類,每一個類針對不一樣的測試用例,而每一個測試類中又包含了不少個不一樣的測試方法。面對如此龐大的一個測試工程,一般會花上數十分鐘甚至數小時才能將預約好的全部測試方法跑完,咱們固然不但願看到因爲某一個測試方法失敗而致使剩下的全部測試方法均不能獲得執行。在自動化測試中,測試方法測試失敗的狀況是很廣泛的,成功或失敗都是一種結果,這總比程序運行到一半拋出異常要好得多。
然而,Assert斷言總會在測試失敗的時候拋出異常,從而終止程序運行。以下面的測試方法,若是前兩個斷言中有任何一個失敗的話,則剩下的斷言不會被執行。
[TestMethod] public void CheckVariousSumResults() { Assert.AreEqual(3, this.Sum(1001, 1, 2)); Assert.AreEqual(3, this.Sum(1, 1001, 2)); Assert.AreEqual(3, this.Sum(1, 2, 1001)); }
一個有效的解決辦法是將每個斷言分別放到不一樣的測試方法中,以下面的代碼:
[TestMethod] public void Sum_1001AsFirstParam_Returns3() { Assert.AreEqual(3, this.Sum(1001, 1, 2)); } [TestMethod] public void Sum_1001AsMiddleParam_Returns3() { Assert.AreEqual(3, this.Sum(1, 1001, 2)); } [TestMethod] public void Sum_1001AsThirdParam_Returns3() { Assert.AreEqual(3, this.Sum(1, 2, 1001)); }
然而在大多數狀況下這可能行不通。例如你須要測試一個包含100行的table,對每一行的title列進行text測試,在這種狀況下你根本沒法爲每個斷言編寫不一樣的測試方法。首先你沒法肯定測試方法的數量,其次過多的測試方法會增長維護成本。
另外一種我聽到過的解決方法是使用參數化測試,然而據我所知,Coded UI Test中好像並不支持。在其它測試環境中或許有更好的解決辦法。
或許可使用try-catch語句來截獲Assert斷言所拋出的異常,使程序可以繼續運行下去。而後咱們將全部截獲到的異常信息輸出到自定義的文件中,即自定義測試報告!測試報告能夠是任意類型的文檔,記事本或HTML比較經常使用。既然可使用try-catch來截獲Assert斷言的異常欣喜,那麼咱們會很天然地想到使用下面的方法:
[TestMethod] public void CheckVariousSumResults() { MultiAssert.Aggregate( () => Assert.AreEqual(3, this.Sum(1001, 1, 2)), () => Assert.AreEqual(3, this.Sum(1, 1001, 2)), () => Assert.AreEqual(3, this.Sum(1, 2, 1001))); } public static class MultiAssert { public static void Aggregate(params Action[] actions) { var exceptions = new List<AssertFailedException>(); foreach (var action in actions) { try { action(); } catch (AssertFailedException ex) { exceptions.Add(ex); } } var assertionTexts = exceptions.Select(assertFailedException => assertFailedException.Message); if (0 != assertionTexts.Count()) { throw new AssertFailedException( assertionTexts.Aggregate( (aggregatedMessage, next) => aggregatedMessage + Environment.NewLine + next)); } } }
上面的代碼能夠頗有效地解決問題,但仍會存在問題。MultiAssert.Aggreate()方法中過多的斷言最終會將全部的異常信息拋出,這會大大下降異常信息的可讀性,不太利於咱們從測試測試報告中分析出錯的緣由。要知道,測試方法最終的目的不是要讓測試程序運行經過,而是經過測試報告來分析被測試對象可能具備的問題。
下面是一個例子,能夠用來有效地解決上面提出的問題。
public static class AssertWrapper { public static string AreEqual<T>(T expected, T actual, string message) { string result = null; try { Assert.AreEqual(expected, actual, message); TestLog.WritePass(message); } catch (AssertFailedException ex) { result = ex.Message; TestLog.WriteError(message); } catch (Exception ex) { result = ex.Message; TestLog.WriteError(message); } return result; } public static string AreEqual(string expected, string actual, string message) { string result = null; try { Assert.AreEqual(expected, actual, message); TestLog.WritePass(message); } catch (AssertFailedException ex) { result = ex.Message; TestLog.WriteError(result); } catch (Exception ex) { result = ex.Message; TestLog.WriteError(result); } return result; } public static string AreEqual(string expected, string actual, bool ignorecase, string message) { string result = null; try { Assert.AreEqual(expected, actual, ignorecase, message); TestLog.WritePass(message); } catch (AssertFailedException ex) { result = ex.Message; TestLog.WriteError(result); } catch (Exception ex) { result = ex.Message; TestLog.WriteError(result); } return result; } public static string Fail(string message) { string result = null; try { Assert.Fail(message); } catch (AssertFailedException ex) { result = ex.Message; TestLog.WriteError(result); } return result; } }
AssertWrapper類中的方法能夠有多個重載,以知足不一樣的須要,其基本思想就是使用try-catch語句來截獲Assert斷言所拋出的異常。TestLog類中的方法負責寫測試報告,你能夠將測試報告定義成任何形式。而後定義一個TestSettings類用來收集測試工程中全部的測試斷言。
public class TestSettings { public static void AddResult(List<string> resultList, string result) { if (result != null) { if (resultList == null) { resultList = new List<string>(); } resultList.Add(result); } } }
在每個.uitest文件的類中,這樣使用上面的方法:
public List<string> faillist; public void ValidateHeader() { TestSettings.AddResult(faillist,AssertWrapper.AreEqual(true, uIHeader.Exists, "test page: Validate Page header text")); }
而後,在全部的測試方法中添加下面的代碼(faillist爲泛型List對象,被定義爲TestMethod所在的類的私有變量,同時咱們經過faillist.AddRange(testPage.faillist)語句將測試頁面類中的泛型List內容添加過來):
if (faillist != null && faillist.Count > 0) { StringBuilder fail = new StringBuilder(); foreach (string s in faillist) { fail.AppendLine(s); } Assert.Fail(fail.ToString()); }
這樣,能夠對該測試方法中包含的全部Assert斷言進行統一管理。這樣作有幾個好處:
Coded UI Test如何搜索一個控件?
在Coded UI Test中,最多見的問題是如何找到被測試的控件。只有找到被測試的對象,才能使用斷言來判斷其中的屬性是否知足預期的值。大多數狀況下,咱們都會使用Coded UI Test Builder窗口來捕獲UI上的控件,但有些狀況下咱們不得不自行搜索須要的控件。一個簡單的例子,在列表控件中如何查找第一個子元素中所包含的文本。就像本文一開始給出的測試需求。若是你經過Coded UI Test Builder直接查找第一個子元素,其中生成的搜索條件每每具備特定性,當頁面的條件發生變化,特定的搜索條件不必定能找到對應的控件。
查看.Designer.cs文件中自動生成的代碼,全部控件的定義都會包含相似於下面代碼的搜索條件:
HtmlEdit mUIEmailEdit = new HtmlEdit(someAncestorControl); mUIEmailEdit.SearchProperties[HtmlEdit.PropertyNames.Id] = "email"; mUIEmailEdit.SearchProperties[HtmlEdit.PropertyNames.Name] = "email"; mUIEmailEdit.FilterProperties[HtmlEdit.PropertyNames.LabeledBy] = null; mUIEmailEdit.FilterProperties[HtmlEdit.PropertyNames.Type] = "SINGLELINE"; mUIEmailEdit.FilterProperties[HtmlEdit.PropertyNames.Title] = null; mUIEmailEdit.FilterProperties[HtmlEdit.PropertyNames.Class] = null; mUIEmailEdit.FilterProperties[HtmlEdit.PropertyNames.ControlDefinition] = "id=email size=25 name=email"; mUIEmailEdit.FilterProperties[HtmlEdit.PropertyNames.TagInstance] = "7"; mUIEmailEdit.Find();
Coded UI Test會試圖經過全部已知的條件來搜索指定的控件,它使用廣度優先查找方法(Breadth-First)。全部SearchProperties能夠被視爲使用AND條件進行查找,若是經過SearhProperties找到一個或未找到對應的控件,則全部的FilterProperties條件不會被使用。若是經過全部的SearchProperties條件找到多個對應的控件,則嘗試逐個使用給出的FilterProperties條件進行按序匹配(Ordered match),直到找到匹配的控件。若是經過以上給出的全部條件最終找到多餘一個的匹配項,則第一個匹配的元素即爲找到的控件。
在上面給出的例子中,會按照以下順利進行搜索:
下面的流程圖說明了這一過程:
有一點須要注意:
在Web controls中,搜索條件的使用可能會涉及到瀏覽器兼容性問題。如篩選條件最終須要經過InnerText來肯定控件,而該屬性在某些瀏覽器上並不支持,此時可能引起異常。在程序編碼過程當中嘗試給特定的控件指定ID屬性能夠更好的解決這一問題,這就須要與程序開發人員進行有效的溝通。從這一點也能夠看出,測試驅動開發的重要性。
不要嘗試經過GetChildren()方法來遍歷全部的控件,由於該方法返回結果會很慢,尤爲是當頁面中存在大量控件時。可使用臨近的祖先節點對該控件進行定義(構造函數的參數能夠用來指定被搜索控件的祖先),而後經過給定SearchProperties或FilterProperties來對控件進行篩選,而後使用FindMatchingControls()方法來肯定要搜索的控件。以下面的代碼用來遍歷Table元素從而找到表中全部的<th/>和<td/>:
HtmlTable uITable = this.UIRelWindow.UIRelDocument.UITable; HtmlRow rowall = new HtmlRow(uITable); UITestControlCollection rows = rowall.FindMatchingControls(); int rowCount = rows.Count; for (int i = 0; i < rowCount; i++) { HtmlHeaderCell allTH = new HtmlHeaderCell(rows[i]); HtmlCell allTD = new HtmlCell(rows[i]); UITestControlCollection THs = allTH.FindMatchingControls(); UITestControlCollection TDs = allTD.FindMatchingControls(); ... ... }
代碼結構調整
.uitest文件針對的是每個測試頁面,每一個頁面都有單獨的驗證方法用來測試頁面上各個不一樣的部分,具備良好結構的代碼可使整個測試工程看起來思路清晰。若是有必要,你徹底可使用設計模式來更加簡練地組織工程中的測試方法和類。一個無缺的測試工程代碼結構看起來像這樣:
public class TestRunner { public TestRunner() { homePage = new UI.HomePageClasses.HomePage(); } #region Home page actions and validate method private UI.HomePageClasses.HomePage homePage; public UI.PageClasses.HomePage HomePage { get { if ((this.homePage == null)) { this.homePage = new UI.PageClasses.HomePage(); } return this.homePage; } set { homePage = value; } } public void LaunchHomePage() { HomePage.LaunchHomePage(new System.Uri(TestSettings.GetCurrentSiteURL())); } public void ValidateHomePageText() { HomePage.ValidateHomePageText(); } }
使用TestRunner類將工程中全部的驗證方法和UI Actions方法進行包裝,而後在測試方法中進行調用。
[TestMethod] public void IncomeStatementsTest() { testrunner.NavigateToTestPage(); testrunner.ValidateSomething(); } [TestInitialize()] public void MyTestInitialize() { testrunner = new TestRunner(); testrunner.LaunchHomePage(); } ////Use TestCleanup to run code after each test has run [TestCleanup()] public void MyTestCleanup() { testrunner = null; } private TestRunner testrunner;
忘記說明一點,帶有[CodedUITest]特徵屬性的類中,咱們能夠借用MyTestInitialize()方法和MyTestCleanup()方法進行一些初始化操做和清理工做。不要在該類的構造函數中添加任何代碼,經過帶有[TestInitialize]特徵屬性的方法進行初始化工做。一樣,帶有[TestCleanup]特徵屬性的方法能夠用來進行一些清理工做。
另外,和大多數工程同樣,Coded UI Test工程容許使用App.config文件。在工程中添加該文件並加入<appSettings></appSettings>節點以設置配置信息。
<configuration> <appSettings> <add key ="" value=""/> </appSettings> </configuration>
如何使用命令行方式運行測試方法?
除了在Visual Studio中運行測試方法外,咱們還能夠經過其它許多方式來運行測試方法。使用測試代理和測試控制器能夠對全部的測試方法進行有效管理,並能夠將測試方法分發到不一樣的測試機上單獨進行測試,但須要在服務器上進行部署,MSDN上有相應的介紹,這裏主要介紹如何經過命令行方式來運行測試方法。
MSTest /testcontainer:CodedUITestProject2.dll /test:CodedUITest1.CodedUITestMethod1
msdn上有對MSTest.exe命令行全部參數的說明。有幾點須要說明一下:
若是你想分發你的測試工程在其它機器上運行,能夠編寫.bat文件並將Coded UI Test工程生成的.dll文件放到同一文件夾下。.bat文件的內容看起來像下面這樣:
@echo off @set PATH=c:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE;%PATH% echo ****** This program will start a Coded UI Test Method ****** pause MSTest /testcontainer:CodedUITest1.dll /test:CodedUITest1.CodedUITestMethod1 echo ****** End Coded UI Test Method ******** pause