有關什么是Coded UI Test以及如何使用Coded UI Test可以查看我的另一篇文章:http://www.cnblogs.com/jaxu/p/3706652.html
本文主要介紹如何在Coded UI Test中使用數據驅動測試。考慮這樣一個場景:開發人員提交了一個函數,該函數實現了一個數學公式的運算,通過接收兩個數字并進行數學運算給出結果。測試人員根據給定的數學公式,需要考慮提供各種不同情況的值來循環測試該函數。顯然,我們需要提供一個數據源,根據數據源中提供的不同的值來進行自動化測試。這是最常見的數據驅動測試的案例。在基于Coded UI Test的Webpage自動化測試中,瀏覽器兼容性問題是通常要考慮的,我們可以在數據源(數據源可能是一個記事本或者一個簡單的Excel表格)中提供要測試的瀏覽器的名稱和版本號,然后讓Coded UI Test自動加載不同的瀏覽器來循環測試目標頁面。下面的內容會介紹這些方法。
這里有兩篇文章詳細描述了如何通過[DataSource]特征屬性來完成數據驅動Coded UI Test。其基本思想是通過在TestMethod前面添加[DataSource]特征屬性,并指定數據源的類型和位置,然后Coded UI Test的測試方法在運行時會自動讀取數據源中的數據,在迭代中完成比對。
http://blogs.msdn.com/b/mathew_aniyan/archive/2009/03/17/data-driving-coded-ui-tests.aspx
http://msdn.microsoft.com/en-us/library/ms182527.aspx
下面這段代碼說明了這一情況:
[TestMethod] [DataSource("System.Data.Odbc", @"Dsn=Excel Files;dbq=C:\Box Office Results.xlsx;defaultdir=C:;driverid=1046;maxbuffersize=2048;pagetimeout=5", "BoxOfficeResults$", DataAccessMethod.Sequential)] public void MyTest() { int rowIndex = this.TestContext.DataRow.Table.Rows.IndexOf(this.TestContext.DataRow); string s = this.TestContext.DataRow[0].ToString(); }
特征屬性[DataSource]有多個重載,以用來通過不同的方式指定數據源的位置。如果你不知道上述代碼中的數據源連接字符串是如何提供的,可以在Visual Studio中嘗試添加數據源操作,然后拷貝其中自動生成的數據源連接字符串。
- 在Visual Studio中通過VIEW->Other Windows->Data Sources打開數據源窗口
- 點擊添加一個新的數據源,選擇Database
- 點擊New Connection...在打開的窗口中通過ODBC方式選擇Excel文件,Visual Studio會自動為你生成連接字符串
- 拷貝該連接字符串
運行上面的代碼,你會發現其實數據讀取工作是在迭代中完成的。也就是說測試方法會被不斷地迭代,直到數據源中所有行均被讀取完成。數據迭代的方式可通過枚舉DataAccessMethod來指定,一共兩種方式:順序讀取或隨機讀取。
我們將上面代碼中的Excel文件放到C盤根目錄,然后調試代碼。Excel中的第一行默認會被作為標題,數據默認會從第二行開始讀取,所以第一次迭代的時候DataRow[0]返回的是Excel中A2單元格的內容。TestContext對象被作為數據源上下文,通過它你可以找到數據源的一些屬性。例如通過上面代碼中的第一行獲取到數據源的行索引。這里是msdn上有關DataRow屬性的說明http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittesting.testcontext.datarow.aspx
將數據源連接字符串直接寫在代碼里并不是什么明智之舉,那有沒有什么方法可以將它移到配置文件中呢?答案是肯定的!通過msdn的這篇文章我們可以得知如何將數據源連接字符串移到配置文件中http://msdn.microsoft.com/en-us/library/ms243192.aspx
首先在工程中添加Application Configuration File,即App.config。
內容如下:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="microsoft.visualstudio.testtools" type="Microsoft.VisualStudio.TestTools.UnitTesting.TestConfigurationSection, Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/> </configSections> <connectionStrings> <add name="MyExcelConn" providerName="System.Data.Odbc" connectionString="Dsn=Excel Files;dbq=C:\Box Office Results.xlsx;defaultdir=C:;driverid=1046;maxbuffersize=2048;pagetimeout=5"/> </connectionStrings> <microsoft.visualstudio.testtools> <dataSources> <add name="BoxOfficeResults" connectionString="MyExcelConn" dataTableName="BoxOfficeResults$" dataAccessMethod="Sequential"/> </dataSources> </microsoft.visualstudio.testtools> </configuration>
注意microsoft.visualstudio.testtools節中type屬性的版本號可能會由于使用的.NET Framework版本的不同有所變化。2.0為8.0.0.0,3.5為9.0.0.0,3.5以上應該是10.0.0.0。在App.config文件中添加上述配置信息之后,將測試方法的[DataSource]特征屬性改成
[DataSource("BoxOfficeResults")]
如果Excel文件中存在多個表,則可以在配置文件的<dataSources>中添加多個<add>節點,指明不同的數據表名稱和數據讀取方式。
還記得文章一開始提到的使用數據驅動測試來實現多瀏覽器兼容性測試嗎?我們可以通過下面的代碼來實現。
BrowserWindow.CurrentBrowser = this.TestContext.DataRow[0].ToString();
將不同版本的瀏覽器名稱添加到數據源文件中,可以是一個簡單的記事本或者.csv文件,如:
BrowserType
IE
firefox
chrome
注意第一行是標題,[DataSource]特征屬性在讀取數據時始終會將第一行默認為標題行,數據默認是從第二行開始讀取的。瀏覽器只需要提供名稱即可,大小寫沒有關系。如果你的測試方法需要在不同的瀏覽器中完成測試,則可以嘗試該方法,但我不保證其中是否會涉及到兼容性問題,就像我在前一篇文章中提到的Coded UI Test如何在頁面上搜索一個控件,如果搜索的條件存在瀏覽器兼容性問題,則可能會拋出異常。
雖然通過[DataSource]特征屬性可以非常方便地讀取數據源中的數據來完成數據驅動測試,但是有些情況下這種方式并不適用。考慮一個簡單的需求:被測試頁面上有一個表格,其數據來源于服務器上的一個Excel文件。通過編寫基于數據驅動測試的Coded UI Test方法來檢查頁面上顯示的內容是否與Excel文件一致。
由于[DataSource]特征屬性是以迭代的方式來進行數據驅動測試的,因此我們無法在數據迭代的過程中去遍歷頁面UI元素。況且,如果迭代中存在Assert斷言,也不太方便我們輸出測試結果。最終的期望是,遍歷整個UI Table,通過與預先讀取的數據源中的數據進行逐一比對來完成整個測試,其過程可能是這樣:
- 讀取數據源中的數據并緩存。由于無法使用[DataSource]特征屬性自動讀取數據,因此不得不自己編寫代碼來完成數據讀取操作。
- 找到UI Table進行遍歷,將所有單元格的數據緩存。
- 將兩部分緩存的數據進行比對,這可能需要預先提供Mapping以幫助完成數據比對過程。
我們以http://www.cnblogs.com/jaxu/p/3635634.html頁面中的表格為例來看看如何實現這一過程。
下面是UIMap2.uitest中的完整代碼:
public partial class UIMap2 { public void LaunchPage() { this.UIExcelInteractiveViewWindow.LaunchUrl(new Uri("http://www.cnblogs.com/jaxu/p/3635634.html")); } public void TestTableData() { HtmlTable targetTable = this.UIExcelInteractiveViewWindow.UIExcelInteractiveViewDocument.UICnblogs_post_bodyPane.UIItemTable; HtmlRow rowall = new HtmlRow(targetTable); UITestControlCollection rows = rowall.FindMatchingControls(); Dictionary<int, Dictionary<int, string>> PageTableDataCache = new Dictionary<int, Dictionary<int, string>>(); int rowCount = rows.Count; for (int i = 0; i < rowCount; i++) { HtmlCell allTD = new HtmlCell(rows[i]); UITestControlCollection TDs = allTD.FindMatchingControls(); Dictionary<int, string> cellsDictionary = new Dictionary<int, string>(); int tdCount = TDs.Count; for (int j = 0; j < tdCount; j++) { cellsDictionary.Add(j, ((HtmlCell)TDs[j]).InnerText); } PageTableDataCache.Add(i, cellsDictionary); } Dictionary<int, Dictionary<int, string>> ExcelDataCache = GetExcelData(ConfigurationManager.AppSettings["ExcelPath"], "BoxOfficeResults"); // load mapping MappingRow[] mappingRows = GetMappingRowCollection(); string msg = string.Empty; for (int i = 0; i < mappingRows.Length; i++) { Dictionary<int, string> pageRowCellsDictionary = PageTableDataCache[mappingRows[i].PageTableTrIndex]; Dictionary<int, string> excelRowCellsDictionary = ExcelDataCache[mappingRows[i].ExcelRowNum]; MappingColumn[] mappingColumns = mappingRows[i].MappingColumn; for (int j = 0; j < mappingColumns.Length; j++) { string excelValue = excelRowCellsDictionary[mappingColumns[j].ExcelColumnNum]; string pageValue = pageRowCellsDictionary[mappingColumns[j].PageTableTdIndex]; Assert.AreEqual(excelValue, pageValue, string.Format("Validation failed at row {0} column {1}", mappingRows[i].ExcelRowNum, mappingColumns[j].ExcelColumnNum)); } } } private Dictionary<int, Dictionary<int, string>> GetExcelData(string filePath, string sheetName) { Dictionary<int, Dictionary<int, string>> sheetDataDic = new Dictionary<int, Dictionary<int, string>>(); Application excel = new Application(); excel.Visible = false; excel.UserControl = true; try { Workbook wb = (Workbook)excel.Application.Workbooks.Open(filePath, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value ); foreach (var worksheet in wb.Worksheets) { Worksheet ws = ((Worksheet)worksheet); if (ws.Name.Equals(sheetName)) { int rowscount = ws.UsedRange.Cells.Rows.Count; int colscount = ws.UsedRange.Cells.Columns.Count; for (int i = 0; i < rowscount; i++) { Dictionary<int, string> cellsDictionary = new Dictionary<int, string>(); for (int j = 0; j < colscount; j++) { Range range = ws.Cells.get_Range(ConvertNumberToName(j) + (i + 1)); string cellText = ((object)range.Text) == null ? "" : ((object)range.Text).ToString(); cellsDictionary.Add(j, cellText); } sheetDataDic.Add(i, cellsDictionary); } break; } } } finally { excel.Application.Workbooks.Close(); excel.Quit(); System.Runtime.InteropServices.Marshal.ReleaseComObject(excel); excel = null; GC.Collect(); } return sheetDataDic; } private string ConvertNumberToName(int index) { if (index < 0) { throw new Exception("invalid parameter"); } List<string> chars = new List<string>(); do { if (chars.Count > 0) index--; chars.Insert(0, ((char)(index % 26 + (int)'A')).ToString()); index = (int)((index - index % 26) / 26); } while (index > 0); return String.Join(string.Empty, chars.ToArray()); } private MappingRow[] GetMappingRowCollection() { Mapping mapping = null; MappingRow[] mappingRowCollection = null; XmlSerializer serializer = new XmlSerializer(typeof(Mapping)); using (FileStream fs = new FileStream(ConfigurationManager.AppSettings["MappingsPath"], FileMode.Open)) { using (XmlReader reader = XmlReader.Create(fs)) { mapping = (Mapping)serializer.Deserialize(reader); } } if (mapping != null) { mappingRowCollection = mapping.MappingRow; } return mappingRowCollection; } }
其中有兩個public方法:LaunchPage()用來導航到目標頁面;TestTableData()用來遍歷目標頁面上的表格并與數據源Excel中的數據進行比對,給出測試結果。
私有方法GetExcelData()用來讀取Excel文件中的數據,其中用到了Microsoft.Office.Interop.Excel程序集中的對象,需要在工程中單獨添加引用。數據按行和列的方式存放到Dicitionary字典對象中,字典中的Key為每一行的行號,Value則是另一個字典,包含該行所有的列。事實上,程序中的其它地方也用到了這種數據存儲結構。
在TestTableData()方法中,一共完成了三個步驟:
- 首先遍歷頁面上的Table,將單元格的內容緩存到字典對象PageTableDataCache中。該字典對象的數據存儲結構與上面講到的Excel數據存儲結構相同。值得注意的是,在遍歷Table的過程中由于是遍歷Table下面所有HtmlRow的集合,此處不包含Header部分,因此Table的表頭部分的數據不會被存儲到字典對象中。
- 通過GetExcelData()方法將Excel中的數據緩存到字典對象ExcelDataCache中。
- 添加一個Mapping,用來對頁面上的表格和Excel進行映射。為什么需要Mapping?因為頁面上表格中單元格的行和列與Excel中的行和列并不總是一一對應的,所以這里必須要通過Mapping來進行映射,以確定如何進行比對。Mapping可以是一個XML文件,在程序中通過反序列化的方式進行讀取,該操作由私有方法GetMappingRowCollection()完成。下面是Mapping XML文件的內容,有關如何通過Visual Studio自動生成XML反序列化的類,可以查看這篇文章http://www.cnblogs.com/jaxu/p/3632077.html。
<?xml version="1.0" encoding="utf-8" ?> <Mapping> <MappingRow ExcelRowNum="1" PageTableTrIndex="0"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="2" PageTableTrIndex="1"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="3" PageTableTrIndex="2"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="4" PageTableTrIndex="3"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="5" PageTableTrIndex="4"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="6" PageTableTrIndex="5"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="7" PageTableTrIndex="6"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="8" PageTableTrIndex="7"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="9" PageTableTrIndex="8"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> <MappingRow ExcelRowNum="10" PageTableTrIndex="9"> <MappingColumn ExcelColumnNum="0" PageTableTdIndex="0"/> <MappingColumn ExcelColumnNum="1" PageTableTdIndex="1"/> <MappingColumn ExcelColumnNum="2" PageTableTdIndex="2"/> <MappingColumn ExcelColumnNum="3" PageTableTdIndex="3"/> <MappingColumn ExcelColumnNum="4" PageTableTdIndex="4"/> </MappingRow> </Mapping>
同時,由于程序中需要讀取Excel文件以及反序列化Mapping XML文件,需要在App.config文件中添加兩個配置項。
<appSettings> <add key="MappingsPath" value="..\..\..\CodedUITestProject2\Mapping.xml"/> <add key="ExcelPath" value="c:\Box Office Results.xlsx"/> </appSettings>
注意MappingPath的路徑使用的是相對路徑,通過將該文件設置為拷貝到輸出路徑以方便部署。方法是在Visual Studio的Solution Explorer中右鍵選擇該文件->Properties,將Copy to Output Directory改成Copy always。
- 遍歷Mapping中所有行和列,并比較PageTable和Excel中的數據,通過Assert斷言來獲取測試結果。由于Assert斷言在測試失敗時總會拋出異常而終止余下的測試步驟,可以參考文章http://www.cnblogs.com/jaxu/p/3706652.html中有關Assert斷言部分對上述代碼進行改進。
- 添加測試方法以調用LaunchPage()和TestTableData()
[TestMethod] public void MyTest() { UIMap2 uimap = new UIMap2(); uimap.LaunchPage(); uimap.TestTableData(); }
下面的代碼由Visual Studio自動生成并做了少量修改(將byte類型改為int),用來將Mapping XML進行反序列化。
/// <remarks/> [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] [System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)] public partial class Mapping { private MappingRow[] mappingRowField; /// <remarks/> [System.Xml.Serialization.XmlElementAttribute("MappingRow")] public MappingRow[] MappingRow { get { return this.mappingRowField; } set { this.mappingRowField = value; } } } /// <remarks/> [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] public partial class MappingRow { private MappingColumn[] mappingColumnField; private int excelRowNumField; private int pageTableTrIndexField; /// <remarks/> [System.Xml.Serialization.XmlElementAttribute("MappingColumn")] public MappingColumn[] MappingColumn { get { return this.mappingColumnField; } set { this.mappingColumnField = value; } } /// <remarks/> [System.Xml.Serialization.XmlAttributeAttribute()] public int ExcelRowNum { get { return this.excelRowNumField; } set { this.excelRowNumField = value; } } /// <remarks/> [System.Xml.Serialization.XmlAttributeAttribute()] public int PageTableTrIndex { get { return this.pageTableTrIndexField; } set { this.pageTableTrIndexField = value; } } } /// <remarks/> [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] public partial class MappingColumn { private int excelColumnNumField; private int pageTableTdIndexField; /// <remarks/> [System.Xml.Serialization.XmlAttributeAttribute()] public int ExcelColumnNum { get { return this.excelColumnNumField; } set { this.excelColumnNumField = value; } } /// <remarks/> [System.Xml.Serialization.XmlAttributeAttribute()] public int PageTableTdIndex { get { return this.pageTableTdIndexField; } set { this.pageTableTdIndexField = value; } } }
UIMap2.uitest中的代碼實現了Excel數據與頁面Table中的內容比對,并最終通過了測試。不過從嚴格意義上來講,這種情況應該不屬于數據驅動測試的范疇,數據驅動測試的意義在于通過給定的數據來測試程序已有的功能,而上述情況是通過數據源來比對UI中的內容。不過我們仍然可以從中了解到如何編寫代碼來讀取數據源并對Webpage中的表格進行Coded UI test。
文章列表