在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