测试自动化
测试场景系统由一个单一文件组成。我打算把我的测试集实现为一个C#控制台应用程序,但你将会看到,我能使用任何与.Net兼容的语言 (例如,Visual Basic .NET),该技术也能用于任何程序(例如,一个Windows程序)和测试框架(例如,NUnit)。场景的整体结构显示在图3中。首先,我向“Microsoft Internet Controls”这个优秀的COM组件添加了一个引用(reference)。这是shdocvw.dll模块的一个别名,该模块拥有操作基于 Windows的浏览器(例如IE和Windows Explorer)的能力。然后我向Microsoft.mshtml.Net组件添加了一个引用。这是mshtml.dll模块的一个别名,该模块拥有访问HTML元素的能力。我向两个相应的名字空间增加了“using”声明,这样我就不需完整验证他们的类了。我也针对 System.Diagnostics和System.Threading分别增加了“using”声明,这样我容易引用前者的Process类,也能在我合适的时候引用后者的Thread.Slepp方法来暂停我的自动化过程。
这项技术的一个关键是要有能力确定一个Web页面/文档/应用程序何时已经在IE中充分装载了。我定义了一个类范围的AutoResetEvent对象documentComplet,我使用该对象来标记一个已经充分装载了文档的等待线程。
static AutoResetEvent documentComplete = new AutoResetEvent(false);
马上,我就会详细介绍这里的细节。我由向命令行中打印一条状态信息来开始我的测试场景。然后我声明了一个Boolean类型的变量pass并把它设为 false。我假设测试场景会失败,如果我检测到应用程序的最后状态是正确的,我修正我的假设并把pass变量设为true。下一步我声明了一个 InternetExplore对象“ie”:
Console.WriteLine("\nStart test run");
bool pass = false;
InternetExplorer ie = null;
Internet Explorer类是在SHDocVw名字空间中定义的。该类有很多方法操纵Internet Explorer的一个实例,但是由你决定启动Internet Explorer并把两者相关联,如下所示:
// launch explorer
Console.WriteLine("\nLaunching an instance of IE");
Process p = Process.Start("iexplore.exe", "about:blank");
if (p == null) throw new Exception("Could not launch IE");
Console.WriteLine("Process handle = " + p.MainWindowHandle.ToString());
// find all active browsers
SHDocVw.ShellWindows allBrowsers = new SHDocVw.ShellWindows();
Console.WriteLine("Number active browsers = " + allBrowsers.Count);
if (allBrowsers.Count == 0) throw new Exception("Cannot find IE");
我使用System.Diagnostics.Process名字空间中的Start方法来启动一个Internet Explorer(iexplore.exe)并装载空白页面“about:blank”;Start方法返回对创建进程对象的一个引用。然后我初始化了一个名为allBrowsers的ShellWindows对象。这个对象掌握了对所有ShellWindows对象的引用,也掌握了对浏览器的引用(包括Windows Explorer的实例,我刚才启动的Internet Explorer的实例和之前启动的Internet Explorer的实例)。我使用Count属性来显示关于目前活动浏览器的诊断信息,以便确保Internet Explorer成功启动了。测试自动化的下一步是把新的进程与Internet Explorer对象关联起来:
Console.WriteLine("Attaching to IE");
for(int i=0; i < allBrowsers.Count && ie == null; i++)
{
InternetExplorer e = (InternetExplorer)allBrowsers.Item(i);
if (e.HWND == (int)p.MainWindowHandle) ie = e;
}
if (ie == null) throw new Exception("Failed to attach to IE");
可能有好几个Internet Explorer的实例正在运行,所以我需要辨明哪个是我的测试场景启动的,以便我能把我的InternetExplorer变量ie与正确的实例关联起来。记住,我把测试启动的Internet Explorer捕获到一个进程对象p中了。所以我遍历ShellWindows中的每一个对象,检查他们的句柄或指针是否和测试启动的进程的主窗口句柄一致。我有时候采用的一个替换的方法是假设只有我的测试Internet Explorer实例允许运行。如果有多个Internet Explorer实例运行,我抛出一个异常。这个假设运行我把测试Internet Explorer与下面的代码简单关联起来:ie = (InternetExplorer)allBrowsers.Item(0); |
你实际采用何种方法取决于你的实际测试场景。既然我建立了我的测试InternetExplorer对象,我能注册我之前提到的DocumentComplete事件句柄:
ie.DocumentComplete += new DWebBrowserEvents2_DocumentCompleteEventHandler(ie_DocumentComplete); |
简单来说,当InternetExplorer的DocumentComplete事件发生时,调用用户定义的ie.DocumentComplete方法。如果你回头去看图3中的代码,你能看到我如此定义了该方法:
private static void ie_DocumentComplete(object pDisp, ref object URL) { documentComplete.Set(); } |
ie_DocumentComplete 方法调用了我早些时候在测试类中定义的AutoResetEvent对象中的Set方法。简而言之,现在我能暂停我的执行线程,直到我的 InternetExplorer对象充分装载了。我会立即向你展示怎样具体做这件事情。现在我浏览正在进行测试的Web应用程序,等到应用程序充分装载:
Console.WriteLine("\nNavigating to the Web app"); object missing = Type.Missing; ie.Navigate("http://localhost/LowLevelWebUIAutomationApp/WebForm1.aspx", ref missing, ref missing, ref missing, ref missing); documentComplete.WaitOne(); |
我使用InternetExplorer.Navigate方法来装载我的Web应用程序。Navigate接受数个可选参数,但是在这个例子中,我不需要任何参数。注意,我调用了documentComplete对象的WaitOne方法。WaitOne将停止我的执行线程,直到应用程序充分装载到了Internet Explorer中。在这个例子中,我没有提供一个超时值,所以我会不停的等待,但你很可能会向WaitOne传递一个代表超时毫秒数的整型值。下一步我设定Internet Explorer为一个固定的大小,并获得Web应用程序文档的一个引用。
Console.WriteLine("Setting IE to 525x420"); ie.Width = 525; ie.Height = 420; HTMLDocument theDoc = (HTMLDocument)ie.Document; |
我声明了一个HTMLDocument变量,并为它指定了一个值。HTMLDocument接口是在mshtml名字空间中定义的。我怎么知道呢?图 4是Visual Studio .NET对象浏览器的一个屏幕截图。我扩展mshtml interop到汇编层,看到了它们的所有接口、类、事件和其他对象。
图 4 对象浏览器
下一步,我模拟了对“Last Name”单选按钮的检查,向文本框控件中输入“urk”:
Console.WriteLine("\nSelecting ''Last Name'' radio button"); Console.WriteLine("Setting text box to ''urk''"); |
这两段代码是非常相似的,并且看起来相当明白。我通过getElementByID方法获得了HTMLInputElemen对象的引用。在拥有这个对象之后,我能使用它的属性或方法来操纵它。这里我选择单选按钮控件的“checked”属性(因为在C#中checked是一个保留字,我必须使用 “@checked”)和文本框控件的“value”属性。按你下面看到的方式点击Search按钮:
Console.WriteLine("Clicking search button"); HTMLInputElement button = (HTMLInputElement)theDoc.getElementById("Button1"); button.click(); documentComplete.WaitOne(); |
在这个例子中,我需要调用WaitOne方法来确保表示搜索结果的页面被充分装载了。通过一些很小的实验,你会发现你能虚拟操纵任何HTML元素。例如,我能模拟下拉框的选择、超链接的点击,当然在这个测试场景中我无需这样做。在我操作Web应用程序之后,我必须检查最终状态的正确性。
Console.WriteLine("\nSeeking ''Burke, Brian'' in list box"); HTMLSelectElement selElement = (HTMLSelectElement)theDoc.getElementsByTagName("select").item(0, null); if (selElement.innerText.ToString().IndexOf("Burke, Brian") >= 0) { Console.WriteLine("Found target string"); pass = true; } else { Console.WriteLine("*Target string not found*"); } |
一般模式是通过共同的标签名获得一个HTML元素集的引用,然后使用属性得到某一特定元素,然后获取表示该元素头标签和尾标签之间数据串的 innerText。这里,我获得了所有<select>元素的引用,然后通过元素集的属性得到了第一个<select>元素 ——当然,也是我的Web页面中唯一的<select>元素。这是由一个ASP.NET列表框控件生成的HTML。传递给项目属性的参数有点复杂。第一个参数既可以是整数,也可以是字符串,前者被理解为基于0的索引值,后者被理解为标签名。我向项目属性的第二个参数传递一个空值NULL。这个参数也是一个索引值,但它只是在项目属性返回一个集合而不是一个原子对象时才有用。有时你需要访问不是任何HTML字元素中的文档体中的值,下面的代码片断向你展示了一种达到目的的方法:
Console.WriteLine("Seeking ''Search complete'' in body"); HTMLBody body = (HTMLBody)theDoc.getElementsByTagName("body").item(0, null); if (body.createTextRange().findText("Search complete", 0, 0) == true) { Console.WriteLine("Found target string"); pass = true; } else { Console.WriteLine("*Target string not found*"); } |