Coded UI Test中的数据驱动测试
有关什么是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。