65.9K
CodeProject 正在变化。 阅读更多。
Home

XML驱动的.NET自动化用户界面测试入门 使用Microsoft UI Automation

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (11投票s)

2014年1月8日

CPOL

33分钟阅读

viewsIcon

30358

downloadIcon

329

这是一个双重工作的示例,其中包含相同的C#和C++/CLR测试框架,它们从相同的XML输入读取测试数据,并在通用的Windows Forms应用程序上执行测试。

目标

本文的目的是从头开始演示如何创建一对测试框架,以便它们都使用XML输入的数据在通用的WindowsForms应用程序上执行相同的测试。在文章中,我将介绍:

  • C#和Visual C++/CLI中的示例。
  • 多程序集应用程序的自动化测试。
  • 在Test Explorer中创建和运行测试的全面方法。
  • 使用TestMethod名称从XML获取测试数据以形成XPath查询。
  • 从组合框中选择,其中一个组合框的内容取决于其前一个组合框中选择的值。
  • 文本框填充。
  • 命令按钮点击。
  • 菜单栏操作。
  • 表单之间的导航。

示例起源

该示例由我正在开发的一个系统驱动,我需要在每天创建超过400个采购和消费交易,分布在四个不同的实例上。如果我一个人做,这将耗尽我用于进一步开发最终功能和报告的所有可用时间,因此,在这种需求下,用户界面自动化是唯一的选择。

由于我使用的是Express产品,我无法录制测试。像许多其他人一样,在网上搜索发现了理论和示例的混乱,几乎没有提供清晰的入门说明。

此外,它们也没有展示如何使用Test Explorer设置测试项目,并且都局限于测试单个表单示例,其中呈现的表单与调用的过程相关联。在我的示例中,与我将调用的过程相关的表单排在第三位!

我在这里的贡献主要源于这些优秀的材料来源,它们对我入门至关重要。

  • [TATAR] - "WPF UI Automation" 作者:Calin Tatar,来自CodeProject
  • [KAILA] - "Automate your UI using the Microsoft Automation Framework" 作者:Ashish Kaila,也来自CodeProject
  • [JOSHI] - 《Beginning XML with C# 2008 From Novice to Professional》 作者:Bipin Joshi
  • [FRASER] - 《Pro Visual C++/CLI and the .NET 3.5 Platform》 作者:Stephen R G Fraser
  • [SANSANWAL] - "Logging method name in .NET" 作者:S Sansanwal,同样来自Codeproject
  • [MCCAFFREY] - "Test Automation with the New menuStrip Control" 作者:James D. McCaffrey 博士

[TATAR] 给了我一个好的开始,并且由于我将如何演变我的解决方案,使用XML很可能最接近我最终生产版本的外观。但最初我从中了解到的很少,除了将硬编码的值插入到一个已经运行的应用程序中。

[KAILA] 另一方面,它在测试本身内部启动被测应用程序,并利用Visual Studio Test Explorer,这从一开始就引起了我的兴趣。两者都提供了Microsoft UI Automation Framework的良好理论解释,所以我不会在这里重新讨论。我从[KAILA]那里学会了使用Automation树,但他使用了Spy++来识别表单属性。这与Windows计算器效果很好,因为元素ID不会改变,但我很快发现我无法依赖这些值,因为我的被测应用程序每次运行时都会被分配不同的ID。到那时,我已经从[TATAR]那里学到了足够多的知识,可以使用不同表单属性的元素名称。

接下来的文本将介绍我构建自动化测试框架的经历。我构建的示例将同时以C#和Visual C++/CLI形式呈现。

介绍示例应用程序 - 及其起源

我使用我的票务系统中的三个程序集创建了一个示例测试应用程序:主菜单、登录控件表单以及用于选择正在运行的公交服务的表单。尽管该过程是通过编译主菜单生成的EXE启动的,但它是三个表单中最后一个出现的,这给故事增加了额外的曲折。

附带的源代码

我已将应用程序的四个版本(在demo app文件夹中)附加。前两个演示了测试中发现的示例错误,V3用于演示完整的测试框架。V1到V3都用Visual C++/CLI编写,但V4的主窗体是用C#编写的,只是为了说明,只要功能保持不变,被测应用程序使用不同的.NET语言就没有区别。

您还将找到两个版本的测试框架,UnitTestProject1包含C#示例 - 而VTRunServiceSln包含Visual C++/CLI变体。

在任何代码运行之前,请搜索其中的“C:\\SBSB\\Logs\\”和“C:\\SBSB\\Training - C#\\ArticleUIAutomation\\DemoApp”引用,或创建文件夹以满足这些路径。

VTRunServiceSln可能无法立即运行。我发现在将其移动到新文件夹后,我无法重新发现测试。C#实现没有这样的问题。如果情况是这样,请按照我设置Visual C++/CLI示例的步骤进行操作,然后粘贴下载中的最终代码。

要运行示例,Demo App需要进行编译 - 因为它太大了,无法上传。

入门

C#

打开一个新的C#项目,选择TestUnit Test Project,如图所示

如果系统要求您连接到Team Foundation Server,请取消对话框,除非您想走这条路。

我将我的项目命名为UnitTest1,这是Visual Studio为我创建的默认项目。

Visual C++/CLI

打开一个新的Visual C++/CLI项目,选择TestManaged Test Project,如图所示

我将我的项目命名为VTRunServiceSln(而不是图中所示的CPPUnitTest),这是Visual Studio为我创建的默认项目。

C++/CLI中的自动生成代码比C#多得多。

连接到自动化框架

我的框架项目议程上的第一项是将其连接到用户界面自动化框架。在引用中包括:UITestAutomationClientUITestAutomationTypes。示例使用了System.CoreSystem.XMLSystem.XML.Linq,所以这些也包括在内。为Automation和Reflection命名空间添加using子句。

C#

采用自动生成的代码

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
        }
    }
}

并添加

using System.Windows.Automation;
using System.Reflection;

Visual C++/CLI

采用自动生成的代码

#include "stdafx.h"

using namespace System;
using namespace System::Text;
using namespace System::Collections::Generic;
using namespace Microsoft::VisualStudio::TestTools::UnitTesting;

namespace CPPUnitTest
{
	[TestClass]
	public ref class UnitTest
	{
	private:
		TestContext^ testContextInstance;

	public: 
		/// <summary>
		///Gets or sets the test context which provides
		///information about and functionality for the current test run.
		///</summary>
		property Microsoft::VisualStudio::TestTools::UnitTesting::TestContext^ TestContext
		{
			Microsoft::VisualStudio::TestTools::UnitTesting::TestContext^ get()
			{
				return testContextInstance;
			}
			System::Void set(Microsoft::VisualStudio::TestTools::UnitTesting::TestContext^ value)
			{
				testContextInstance = value;
			}
		};

		#pragma region Additional test attributes
		//
		//You can use the following additional attributes as you write your tests:
		//
		//Use ClassInitialize to run code before running the first test in the class
		//[ClassInitialize()]
		//static void MyClassInitialize(TestContext^ testContext) {};
		//
		//Use ClassCleanup to run code after all tests in a class have run
		//[ClassCleanup()]
		//static void MyClassCleanup() {};
		//
		//Use TestInitialize to run code before running each test
		//[TestInitialize()]
		//void MyTestInitialize() {};
		//
		//Use TestCleanup to run code after each test has run
		//[TestCleanup()]
		//void MyTestCleanup() {};
		//
		#pragma endregion 

		[TestMethod]
		void TestMethod1()
		{
			//
			// TODO: Add test logic here
			//
		};
	};
}

并添加

using namespace System::Windows::Automation;
using namespace System::Reflection;

连接到被测应用程序

我喜欢[KAILA]通过专用类连接到被测应用程序的方式。在C#中,这比Visual C++/CLI更干净。

C#

右键单击项目并选择Add Class,即可看到此处显示的对话框

将类名用作VTService.cs - 以反映此类将要加载进行测试的应用程序

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UnitTestProject1
{
    class VTService
    {
    }
}

保留System,并用以下内容替换其他内容

using System.Diagnostics;
using System.Threading;
using System.Windows.Automation;

IDisposable添加到类定义中,使其变为

using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Automation;

namespace UnitTestProject1
{
    class VTService : IDisposable
    {
    }
}

添加一个私有变量来保存VTSerivce进程

        private Process _VTServiceProcess;

以及一个基本的构造函数

        public VTService()
        {
		}

在构造函数中添加一个语句,该语句将启动被测应用程序的进程。

_VTServiceProcess = Process::Start("C:\\SBSB\\Systems\\Debug\\VT_Service.exe");

添加一个“Dispose”方法

        public void Dispose()
        {
            _VTServiceProcess.CloseMainWindow();
            _VTServiceProcess.Dispose();
        }

在UnitTest1.cs中的TestMethod1中添加一个语句,该语句将启动VTService应用程序

            using (VTService vtServ = new VTService())
            {
			}

Visual C++/CLI

右键单击项目并选择Add Class,即可看到此处显示的对话框

将类名用作VTService.cpp - 以反映此类将要加载进行测试的应用程序(类库作为模板不可用 - 可能是使用Express的限制)。

这将生成一个空白的.cpp文件,我们将在其中定义一个类来访问VTService应用程序。

添加此代码

#include "stdafx.h"
using namespace System;
using namespace System::Diagnostics;
using namespace System::Threading;
using namespace System::Windows::Automation;

namespace VTRunServiceTests
{
	private ref class VTService
	{
	};
}

请注意,此处不需要IDisposable - 这是cpp的一个优点。

添加一个私有变量来保存VTService进程

        private:
  Process ^_VTServiceProcess;

以及一个基本的构造函数

	public:
		VTService()
		{
		}

在构造函数中添加一个语句,该语句将启动被测应用程序的进程。

_VTServiceProcess = Process::Start("C:\\SBSB\\Systems\\Debug\\VT_Service.exe");

UnitTest.cppTestMethod1中添加一条指令,该指令将启动应用程序

			VTService ^vtServ = gcnew VTService();
				try
				{
					;
				}
			finally
			{
				delete vtServ;
			}

它无法将VTService识别为有效类!

目前我用以下方法解决这个问题

#include "VTService.cpp"

UnitTest.cpp中 - 我再次将其归因于Express的限制。

第一次试运行

打开您的Test Explorer窗口(如果尚未打开)。编译所有内容,然后选择Test Explorer面板中的Run All,或右键单击TestMethod1并选择Run Selected。您应该看到类似以下内容的出现

我们已成功自动化了应用程序的启动 - 这本身并不是非常壮观,但却是自动化UI测试之路上的一个关键起步步骤。

现在我们需要开始做事了

默认的TestMethod1将用户名字段和密码字段填充为“suser”和“spass”,然后单击登录按钮以测试成功的登录。但在此之前,我们还有更多基础工作要做。

首先,添加一个私有AutomationElement属性

在C#中

        private AutomationElement _ VTServiceAutomationElement;

在Visual C++/CLI中

	AutomationElement ^_ VTServiceAutomationElement;

接下来,我们在构造函数中添加一些代码来发现各种自动化元素

注意
起初,我曾认为ct变量会将生成的树限制为50个元素。事实并非如此。测试框架会尝试50次来查找被测应用程序的第一个表单。name属性中的值是第一个出现的表单的标题。在我的示例应用程序中,这来自登录程序集:"Enter your Logon Details"

包含一个睡眠,强制处理等待自动化元素可用。

在C#中

 int ct = 0;
 do
 {
     _VTServiceAutomationElement = AutomationElement.RootElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Enter your Logon Details));
     ++ct;
     Thread.Sleep(100);
 }
 while (_VTServiceAutomationElement == null && ct < 50);

在Visual C++/CLI中

int ct = 0;
do
{
	_VTServiceAutomationElement = AutomationElement::RootElement::FindFirst(TreeScope::Children, gcnew PropertyCondition(AutomationElement::NameProperty, "Enter your Logon Details"));
	++ct;
	Thread::Sleep(100);
}
while (_VTServiceAutomationElement == nullptr && ct < 50);

如果未发现任何元素,报告错误也是个好主意。

在C#中

if (_VTServiceAutomationElement == null)
{
    throw new InvalidOperationException("VT_Service must be running");
}

在Visual C++/CLI中

if (_VTServiceAutomationElement == nullptr)
{
	throw gcnew InvalidOperationException("VT_Service must be running");
}

屏幕上的所有内容都是Windows桌面树的一部分,但我的兴趣仅限于我示例应用程序的子树。在此练习阶段,我所做的只是启动示例应用程序并在UI树中找到它想要开始测试的根。如测试方法所示,没有进一步的测试代码,但我已经看到应用相同方法于相同应用程序的两种不同行为,仅仅是.NET版本不同。

C# - 闪过登录表单,进入“Set your Route and Service”,并报告成功完成。

C++/CLI - 打开登录表单并按预期等待,但报告失败!

这引出了自动化测试的难题之一。错误在哪里?是在测试框架还是被测应用程序中?过去,我曾见过测试经理因为这种担忧而不愿接受自动化。在此实例中,怀疑指向两者。我将重构示例应用程序以包含写入文本文件的审计日志,并使用IDE调试器来调试测试框架。如果您一直在按照上述步骤构建您的测试框架,那么您可以使用演示应用程序的V1重现此错误行为。

因此,继续下去,演示应用程序的V2会将活动记录到C:\SBSB\Logs。这一次,错误在演示应用程序中 - 登录程序集需要一个表单关闭事件。测试框架中不同的行为是由于VTService.cs中存在Dispose()方法。这是C#和C++/CLI之间存在差异的领域之一。

第一个测试 - 登录

现在是时候开始考虑一个测试了。通常,我对任何应用程序的第一个测试是基本的,以证明功能有效到可以进行有意义的测试。对于这里的被测应用程序,那将是向用户名和密码文本框发布有效值,点击登录确定按钮,然后进入下一个表单。毕竟,如果我无法登录,那么尝试更多就没有多大意义了。

首先,我在VTService类中为其声明一个私有成员变量,类型为AutomationElement

在C#中

private AutomationElement _usernameTextBoxAutomationElement;

在Visual C++/CLI中

private AutomationElement^ _usernameTextBoxAutomationElement;

然后告诉构造函数去查找它在元素树中的位置

在C#中

_usernameTextBoxAutomationElement = _VTServiceAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "Username"));

if (_usernameTextBoxAutomationElement == null)
{
    throw new InvalidOperationException("Could not find username box");
}

在Visual C++/CLI中

_usernameTextBoxAutomationElement = _VTServiceAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::AutomationIdProperty, "Username"));

if (_usernameTextBoxAutomationElement == nullptr)
{
	throw gcnew InvalidOperationException("Could not find password box");
}

这个难题的最后一块是一个公共对象,用于与成员变量进行接口

在C#中

public object Username
{
    get
    {
        return _usernameTextBoxAutomationElement.GetCurrentPropertyValue(AutomationElement.NameProperty);
    }
     set
    {
        if (_usernameTextBoxAutomationElement != null)
        {
            LogEntry("Setting Username value...");
            try
            {
                ValuePattern valuePatternU = _usernameTextBoxAutomationElement.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
                valuePatternU.SetValue(value.ToString());
                LogEntry("Username value set");
            }
            catch (Exception e1)
            {
                 LogEntry("Error " + e1.Message + " setting Username Value in " + _usernameTextBoxAutomationElement.Current.AutomationId.ToString());
            }
        }
     }
}

在Visual C++/CLI中

	   property Object ^Username
		{
			Object ^get()
			{
				return _usernameTextBoxAutomationElement->GetCurrentPropertyValue(AutomationElement::NameProperty);
			}
			void set(Object ^value)
			{
				if (_usernameTextBoxAutomationElement != nullptr)
                {
                    LogEntry("Setting Username value...");
					try
					{
						ValuePattern ^valuePatternU = dynamic_cast<ValuePattern^>(_usernameTextBoxAutomationElement->GetCurrentPattern(ValuePattern::Pattern));
						valuePatternU->SetValue(value->ToString());
						LogEntry("Username value set to " + value->ToString());
					}
					catch (Exception ^e1)
					{

						LogEntry("Error " + e1->Message + " setting Username Value in " + _usernameTextBoxAutomationElement->Current.AutomationId->ToString());
                    }
                }
            }
        }

在测试方法中添加一行以填充用户名

在C#中

                vtServ.Username = "suser";

在Visual C++/CLI中

                vtServ->Username = "suser";

当我现在运行测试时,我期望看到“suser”出现在用户名框中。然而,用户名框是空白的!这次我可以相当确定问题出在测试框架上,查看日志文件很快就证实了这一点 - 我试图填充lblUsername,即标识我用户名框的标签。发生这种情况是因为我选择通过名称来标识用户名框,但作为技术实体,名称属于表单上标记用户名框的标签。

字段上没有标题,因此无法通过名称查找。我可以包含一些默认文本在用户名文本框中,这将唯一地标识它来解决这个问题,但我选择不这样做。计算器示例讨论了使用Spy++来确定calc.exe组件的元素ID,但是我的应用程序的连续运行在每次运行时都会为用户名文本框分配不同的元素ID。我需要一个既一致又不要求修改被测应用程序的标识符。

更改

在C#中

_usernameTextBoxAutomationElement = _VTServiceAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "Username"));

在Visual C++/CLI中

_usernameTextBoxAutomationElement = _VTServiceAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::NameProperty, "Username"));

改为

在C#中

_usernameTextBoxAutomationElement = _VTServiceAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement. AutomationIdProperty, "Username"));

在Visual C++/CLI中

_usernameTextBoxAutomationElement = _VTServiceAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::AutomationIdProperty, "Username"));

再次运行测试,"suser"出现在用户名框中。为什么我不需要将"Username"更改为"txtUsername"?答案很简单 - 我的登录表单中的命名约定不佳。标签以"lbl"为前缀,但我没有继续使用"txt"作为文本框的前缀,结果导致一个场景,其中一个特定的单词(在本例中为"Username")是某个属性的名称,而另一个属性的标识符。

密码的填充方式类似。

添加XML到混合体中

当我再次运行测试时,用户名和密码框已填充。虽然这很好,但由于必须编辑测试或复制它来验证另一个用户,它的价值被削弱了。所以在我继续查看如何点击登录按钮之前,我将检查从XML读取用户名和密码。(Excel也可以使用 - 但它强制要求必须存在并安装Excel)。

计算器示例正在测试表达式,因此它能够利用ExpressionTree库来实现其目的。

在解决方案资源管理器中右键单击解决方案名称(UnitTestProject1),然后选择Add -> New Folder(在C++/CLI变体中为New Filter)。

右键单击Test Data,然后选择Add -> New Item。一直滚动到最底部,您将在XML File附近找到XML File。选择它并单击“Add”。这将在TestData文件夹中添加一个名为XMLFile1的文件。IDE会打开它,当前内容只是

<?xml version="1.0" encoding="utf-8" ?>

在C++/CLI中,它需要Add->New Item->Web来包含一个新的XML文件。

在解决方案资源管理器中将其重命名为“RawTestData”(确保保留.xml扩展名)。您可以根据您对测试的方法自由地构建此文件。我选择了每个测试名称的节点,在每个节点内选择每个测试运行,然后是运行编号,最后叶节点是输入内容。

如果您正在通过文本阅读本文,您会立即注意到XML文档中的第一个测试是PositiveLogonTest,但到目前为止,测试框架只有一个TestMethod1。在处理完XML文件后,我们将解决这个问题。

首先,我将完成我的积极登录测试数据。这是我期望成功登录的用户列表。我以后可以引入负面测试,例如密码错误、用户名错误、仅用户名等 - 但它们不属于本文。

目前,RawTestData.XML看起来是这样的

<?xml version="1.0" encoding="utf-8" ?>
<VTServiceTests>
  <PositiveLogonTest>
    <TestRun1>
      <Username>suser</Username>
      <Password>spass</Password>
    </TestRun1>
    <TestRun2>
      <Username>auser</Username>
      <Password>apass</Password>
    </TestRun2>
  </PositiveLogonTest>
</VTServiceTests>

在解决方案资源管理器中右键单击RawTestData.xml,然后选择Properties。我选择了“Copy if Newer”。请注意,我还必须将文件打开并置于IDE的“最顶层”才能使属性可编辑。

返回到UnitTest1.cs(和UnitTest1.cpp),现在是时候解决第一个测试的名称问题了。将定义public void TestMethod1()更改为public void PositiveLogonTest()。按F7可以快速确认TestExplorer中测试名称的更改。

为了采用这种读取测试数据的方法,我需要将XML库的引用添加到UnitTest1.cs(和UnitTest1.cpp)的顶部。

在C#中

using System.Xml; // for the test data.
using System.Xml.XPath; // for the test data access using Xpath.

在Visual C++/CLI中

using namespace System::Xml; // for the test data.
using namespace System::Xml::XPath; // for the test data access using Xpath.

而且,因为我想要提取测试方法的名称以与XML文档中的测试数据进行通信,所以我需要添加

在C#中

using System.Diagnostics; //Used with Reflection for the method name

在Visual C++/CLI中

using namespace System::Diagnostics; //Used with Reflection for the method name)

此提取方法名称的功能也需要Reflection - 但我已将其包含在内。

由于XML在这里仅作为管理测试数据的工具出现,我将尽可能简要地描述我将如何处理它。

首先,UnitTest1类需要一些额外的成员变量

在C#中

        private String m_strNow;
        private StreamWriter m_sw;
        private String m_Filename;
        private XmlDocument m_XMLDoc;
        private XmlNode m_Node;
        private XPathNavigator m_Navigator;
        private StackFrame m_StackFrame;
        private MethodBase m_MethodBase;

在Visual C++/CLI中

	private:
        String^ m_strNow;
        StreamWriter^ m_sw;
        String^ m_Filename;
        XmlDocument^ m_XMLDoc;
        XmlNode^ m_Node;
        XPathNavigator^ m_Navigator;
        StackFrame^ m_StackFrame;
        MethodBase^ m_MethodBase;

我在UnitTest1中添加了一个新方法来访问测试数据文档

在C#中

        private void SetUpTestDataDocument()
        {
            m_Filename = "TestData\\RawTestData.xml";
            m_XMLDoc = new XmlDocument();
            XmlReader reader;
            reader = XmlReader.Create(m_Filename);
            m_XMLDoc.Load(reader);
            reader.Close();
            m_Node = m_XMLDoc.FirstChild; //go to the root of the XML tree.

        }

在Visual C++/CLI中

		void SetUpTestDataDocument()
		{
			m_sw->WriteLine("\n[{0}] - Looking for XML Document ...", m_strNow);
			m_sw->Flush();
			// While the calculator example uses an array of files, just one will be used here.
			m_Filename = "..\\VTRunServiceTests\\RawTestData.xml";
			m_sw->WriteLine("\n[{0}] - XML Document: {1}", m_strNow, m_Filename);
			m_sw->Flush();
			m_XMLDoc = gcnew XmlDocument();
			XmlReader ^reader;
			m_sw->WriteLine("\n[{0}] - Document and reader defined", m_strNow);
			m_sw->Flush();
			reader = XmlReader::Create(m_Filename);
			m_sw->WriteLine("\n[{0}] - reader created", m_strNow);
			m_sw->Flush();
			m_XMLDoc->Load(reader);
			reader->Close();
			m_Node = m_XMLDoc->FirstChild; //go to the root of the XML tree.
			m_sw->WriteLine("\n[{0}] - XML Document opened!", m_strNow);
			m_sw->Flush();
		}

这取自[FRASER]。测试方法PositiveLogonTest()调用这个新的SetUpTestDataDocument ()方法,并设置一个Method Base来提取其名称。

在C#中

            m_StackFrame = new StackFrame();
            m_MethodBase = m_StackFrame.GetMethod();

在Visual C++/CLI中

            m_StackFrame = gcnew StackFrame();
            m_MethodBase = m_StackFrame->GetMethod();

然后,我将PositiveLogonTest ()方法中硬编码的用户名/密码填充替换为嵌套循环,利用XPath,基于[JOSHI]的示例。

在C#中

   // Identify the node using an Xpath expression
   // Build the XML node name using the name of the current method
   String XPathNodeStr = "//" + m_MethodBase.Name;
   XmlNode tmpNode = m_XMLDoc.SelectSingleNode(XPathNodeStr);
   if (tmpNode.HasChildNodes)
   {
      // Use the xpath navigator to traverse the subtree.
      m_Navigator = tmpNode.CreateNavigator();
      m_Navigator.MoveToFirstChild(); // Now position the reader at the first child.*/
   }

   if (m_Navigator.HasChildren)
   {
       do
       {
          m_Navigator.MoveToFirstChild();
          vtServ.Clear();
          do
          {
             if (m_Navigator.Name == "Username")
             {
                m_Navigator.MoveToFirstChild();
                vtServ.Username = m_Navigator.Value;
             }
             else
             if (m_Navigator.Name == "Password")
             {
                 m_Navigator.MoveToFirstChild();
                   vtServ.Password = m_Navigator.Value;
               }
               else
               {
                    m_sw.WriteLine("\n[{0}] - Unknown node", m_strNow);
                    m_sw.Flush();
               }
               m_Navigator.MoveToParent();
          } while (m_Navigator.MoveToNext());
          m_Navigator.MoveToParent();
      } while (m_Navigator.MoveToNext());
 }

在Visual C++/CLI中

try
{
    // Identify the node using an Xpath expression
    // Build the XML node name using the name of the current method
	String ^XPathNodeStr = "//" + m_MethodBase->Name;
	XmlNode ^tmpNode = m_XMLDoc->SelectSingleNode(XPathNodeStr);
	if (tmpNode->HasChildNodes)
	{
		// Use the xpath navigator to traverse the subtree.
		m_Navigator = tmpNode->CreateNavigator();
		m_Navigator->MoveToFirstChild(); // Now position the reader at the first child.*/
		m_sw->WriteLine("\n[{0}] - Navigator created successfully!", m_strNow);
		m_sw->Flush();
	}
	if (m_Navigator->HasChildren)
	{
		do
		{
		    m_Navigator->MoveToFirstChild();
		    vtServ->Clear();
			do
			{
				if (m_Navigator->Name == "Username")
				{
					m_Navigator->MoveToFirstChild();
					vtServ->Username = m_Navigator->Value;
				}
				else
					if (m_Navigator->Name == "Password")
					{
						m_Navigator->MoveToFirstChild();
						vtServ->Password = m_Navigator->Value;
					}
					else
					{
						m_sw->WriteLine("\n[{0}] - Unknown node", m_strNow);
						m_sw->Flush();
					}
				m_Navigator->MoveToParent();
			} while (m_Navigator->MoveToNext());
			m_Navigator->MoveToParent();
		} while (m_Navigator->MoveToNext());
  }
}
finally
{
	delete vtServ;
}

请注意,[FRASER]提供了一个简洁的递归等价物,但这个更适合我在这里想要实现的目标。

首先,我创建一个XPath字符串,使用“//”和测试方法的名称。然后将其插入XML文档中,以查看是否有相应的节点。当该节点有子节点时,我将构建一个XPath导航器,该导航器以该节点为根。我已指示导航器转到包含测试数据的节点(XML上的NodeTestRun1)的第一个子节点。

现在测试此节点是否有子节点,并指示再次移动到第一个子节点,从而到达用户名节点,但我尚未准备好提取值。这是因为XML文档的.NET表示法将节点的值存储在称为“Text”的另一个子节点中。因此,我再向下移动一个级别以获取用户名样本测试值。密码也一样。树的遍历是通过将当前节点移回到其父节点并执行移动下一个操作来控制的,直到树完全遍历。

现在是时候开始点击按钮了。查看计算器示例,以及它如何拥有一个可以用来点击任何数字的通用函数,我想,XML文档可以扩展,除了数据内容之外,还可以提供应用程序元素(如表单名称、文本框和按钮操作)的详细信息,以实现一个真正通用的测试框架 - 但这留待以后。

为了开始点击按钮,我需要在控制我正在测试的应用程序的VTService类中添加两个函数。我将它们命名为GetLogOnFrmButtonGetInvokePattern

GetLogOnFrmButton只处理“Log On”和“Exit”按钮。当它收到处理其中一个的命令时,它会从VTService的自动化树中提取一个按钮元素属性,并将其返回给调用它的方法。

在C#中

  public AutomationElement GetLogOnFrmButton(String argBtnName)
  {
      // Note These Get...FrmButton functions could be rolled up into a single one
      if ((argBtnName != "LogOn") && (argBtnName != "Exit"))
      {
          LogEntry("Only valid buttons are LogOn and Exit");
          throw new InvalidOperationException("Only valid buttons are LogOn and Exit");
      }

      AutomationElement buttonElement = _VTServiceAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, argBtnName));

      if (buttonElement == null)
      {
          LogEntry("Could not find button corresponding to " + argBtnName);
          throw new InvalidOperationException("Could not find button corresponding to " + argBtnName);
      }

      return buttonElement;
  }

在Visual C++/CLI中

AutomationElement ^GetLogOnFrmButton(String ^argBtnName)
{
	// Note These Get...FrmButton functions could be rolled up into a single one
	if ((argBtnName != "LogOn") && (argBtnName != "Exit"))
	{
		LogEntry("Only valid buttons are LogOn and Exit");
		throw gcnew InvalidOperationException("Only valid buttons are LogOn and Exit");
	}

	AutomationElement ^buttonElement = _VTServiceAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::AutomationIdProperty, argBtnName));

	if (buttonElement == nullptr)
	{
		LogEntry("Could not find button corresponding to " + argBtnName);
		throw gcnew InvalidOperationException("Could not find button corresponding to " + argBtnName);
	}

	return buttonElement;
}

GetInvokePattern处理对传入元素执行操作的指令。

在C#中

  public InvokePattern GetInvokePattern(AutomationElement element)
  {
      return element.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
  }

在Visual C++/CLI中

InvokePattern ^GetInvokePattern(AutomationElement ^element)
{
	return dynamic_cast<InvokePattern^>(element->GetCurrentPattern(InvokePattern::Pattern));
}
错误 Link1255 and Link2022
如果您在添加GetInvokePatternVTSetServiceLaunched之类的函数后编译Visual C++/CLI版本,您可能会遇到Link error Link1255和Link2022。在Google上快速搜索表明它们很难确定,例如一个建议是确保所有程序集都以正确的版本编译 - 我们只有一个。另一个建议是将*Debug Configuration*中的编译开关从/MD更改为/MDd。我什么也没做。我继续编码,并在第一次遇到错误时,以及在第二次遇到它时,都添加了一个对受影响函数的调用。在每种情况下,在编码调用函数后,错误都会自行解决。

现在我已经有了点击按钮的工具。我将点击指令放在遍历XML文档的while循环中,紧跟在填充密码字段之后。

在C#中

vtServ.GetInvokePattern(vtServ.GetLogOnFrmButton("LogOn")).Invoke();

在Visual C++/CLI中

vtServ->GetInvokePattern(vtServ->GetLogOnFrmButton("LogOn"))->Invoke();

这段代码在风格、结构或可读性方面不会获奖,但它完成了工作。

如果您正在边阅读边构建代码,请在XML文档中注释掉下一对用户名/密码,然后再次运行测试。

    <!--<TestRun2>
      <Username>auser</Username>
      <Password>apass</Password>
    </TestRun2>-->

这是必要的,因为测试运行将使我们离开登录表单,所以在我包含返回那里的逻辑之前,我将示例限制为单个用户名/密码对。下一次运行将使我们首次看到Set Service表单。

登录后。

因为这只是一个“登录”测试,我打算通过取消此表单来继续,但我不能简单地调用取消按钮,因为此表单不在我之前拍摄的树快照中。

题外话
正是通过编写处理此阶段的代码,我才意识到不同方法的可重用性有多高。所以,如果我愿意妥协,有一个单一的测试方法,或者一个标准的方法按照[TATAR]的指示调用测试库,那么我就有机会通过XML文档驱动一系列标准方法。

所以,我将首先包含代码,该代码将通过VTService类中的VTSetServiceLaunched来确定SetService已被启动。

在C#中

        private AutomationElement _VTSetServiceAutomationElement;

public void VTSetServiceLaunched()
{
    int ct = 0;
    do
    {
        _VTSetServiceAutomationElement = AutomationElement.RootElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Set Your Route and Service"));
        ++ct;
        Thread.Sleep(100);
    }
    while (_VTSetServiceAutomationElement == null && ct < 50);

    if (_VTSetServiceAutomationElement == null)
    {
        LogEntry("VTSetService failed to launch");
        throw new InvalidOperationException("VTSetService failed to launch");
    }
}

在Visual C++/CLI中

AutomationElement ^_VTSetServiceAutomationElement;

void VTSetServiceLaunched()
{
	int ct = 0;
	do
	{
		_VTSetServiceAutomationElement = AutomationElement::RootElement->FindFirst(TreeScope::Children, gcnew PropertyCondition(AutomationElement::NameProperty, "Set Your Route and Service"));
		++ct;
		Thread::Sleep(100);
	}
	while (_VTSetServiceAutomationElement == nullptr && ct < 50);

	if (_VTSetServiceAutomationElement == nullptr)
	{
		LogEntry("VTSetService failed to launch");
		throw gcnew InvalidOperationException("VTSetService failed to launch");
	}
}

VTService类还需要一个SetService表单上按钮的处理程序。

在C#中

public AutomationElement GetSetServiceFrmButton(String argBtnName)
{
      // Note These Get...FrmButton functions could be rolled up into a single one
      if ((argBtnName != "btnOK") && (argBtnName != "btnCancel"))
      {
         LogEntry("Only valid buttons are OK and Cancel");
         throw new InvalidOperationException("Only valid buttons are  OK and Cancel");
      }

     AutomationElement buttonElement = _VTSetServiceAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, argBtnName));

     if (buttonElement == null)
     {
        LogEntry("Could not find button corresponding to " + argBtnName);
        throw new InvalidOperationException("Could not find button corresponding to " + argBtnName);
     }

    return buttonElement;
}

在Visual C++/CLI中

AutomationElement ^GetSetServiceFrmButton(String ^argBtnName)
{
	// Note These Get...FrmButton functions could be rolled up into a single one
	if ((argBtnName != "btnOK") && (argBtnName != "btnCancel"))
	{
		LogEntry("Only valid buttons are OK and Cancel");
		throw gcnew InvalidOperationException("Only valid buttons are  OK and Cancel");
	}

	AutomationElement ^buttonElement = _VTSetServiceAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::AutomationIdProperty, argBtnName));

	if (buttonElement == nullptr)
	{
		LogEntry("Could not find button corresponding to " + argBtnName);
		throw gcnew InvalidOperationException("Could not find button corresponding to " + argBtnName);
	}

	return buttonElement;
}

然后我们需要检查SetService是否已启动,并单击其“Cancel”按钮。为简化起见,我在此省略了错误处理 - 但在生产版本中,我不会在不先确保包含表单已启动的情况下尝试单击按钮。此代码位于PositiveLogonTest()方法中,紧跟在单击“Log On”按钮的代码之后。

在C#中

    //Establish that the Set Service form is opened
    vtServ.VTSetServiceLaunched();
    // Cancel the Set Service form
    vtServ.GetInvokePattern(vtServ.GetSetServiceFrmButton("btnCancel")).Invoke();

在Visual C++/CLI中

    //Establish that the Set Service form is opened
    vtServ->VTSetServiceLaunched();
    // Cancel the Set Service form
    vtServ->GetInvokePattern(vtServ->GetSetServiceFrmButton("btnCancel"))->Invoke();

此时,C#成功完成了其测试并进行清理,通过终止它启动的VT_Service进程。Visual C++/CLI需要一些工作,并且在显示主菜单后失败 - 这让我们首次看到了主菜单。

Visual C++/CLI版本的最后一步是调用删除VTService类的实例,但这样做时它不会终止为运行被测应用程序而生成的进程。这可能是一个垃圾回收问题。C#没有这个调用,并且它的自动终止过程的一部分是对被测应用程序执行“Exit”。

标准退出

尽管我写了以上内容,但我对Visual C++/CLI实例目前更满意。C#实例中没有任何内容可以证明在单击“Set Service”表单上的“Cancel”后主表单是否被启动过。因此,我将在此处包含将主表单加载到自动化树并单击其“Exit”按钮的方法。

我从在VTService类中定义私有属性_VTMainAutomationElement开始。通过复制VTSetServiceLaunched创建新方法VTMainLaunched,将“SetService”的实例更改为“Main”,并将“Set Your Route and Service”替换为“On-Board Service”。

为了访问按钮,我复制了方法GetSetServiceFrmButton来创建GetMainFrmButton,将“SetService”替换为“btnOK”,并将“btnCancel”替换为“Logout”和“Exit”。

PositiveLogonTest方法中,在单击“Set Service”表单上的“Cancel”之后,还需要另外两行代码。它们是

在C#中

//Establish that the Main form is opened
vtServ.VTMainLaunched();
// Exit the Main form
vtServ.GetInvokePattern(vtServ.GetMainFrmButton("Exit")).Invoke();

在Visual C++/CLI中

//Establish that the Main form is opened
vtServ->VTMainLaunched();
// Exit the Main form
vtServ->GetInvokePattern(vtServ->GetMainFrmButton("Exit"))->Invoke();

现在,框架的两个版本在被测应用程序上都表现出相同的行为。当测试机器负载很重时,我们可以看到它闪过表单,最后退出,但在配置良好的机器上,这太快了,肉眼无法捕捉,所以是时候考虑一些证据了。

测试证据

当测试导致数据更新或报告生成时,证明目标不再是比较输出与预期结果的简单问题。但像这样测试屏幕流动的场景是无声无息的。我可以为每个表单调用添加计时器延迟,以便我们能够看到它们,虽然这对于一两个案例效果很好,但当需要运行的案例数量增加时,从效率的角度来看,它会适得其反。同样不建议指示测试方法在每个步骤后要求用户确认。

您将在其他地方看到的一些示例使用断言和异常来证明测试是否按预期行为。虽然最终我可能会采用其中一种或两种,但目前我采用的方法是事件日志记录。被测应用程序将为每个打开的表单和每个点击的按钮创建日志条目。这个示例是一个微不足道的演示,我已经将其硬编码,但在生产系统中,我建议配置此日志记录,以便可以按需控制。它不仅可以用于自动化屏幕导航测试,还可以用于在出现特别棘手的错误需要纠正时,精确跟踪用户的系统操作。

此被测应用程序的典型日志

2013-10-02 17:23:09 - =====================================

[2013-10-02 17:23:09] - VT_Service Launched

[2013-10-02 17:23:09] - VT_Service - Call the Logon Form

[2013-10-02 17:23:09] - Logon - Presenting form

[2013-10-02 17:23:09] - Logon - LogOn_Click

[2013-10-02 17:23:09] - Logon - In Validate User

[2013-10-02 17:23:09] - 0:Username [suser] verified

[2013-10-02 17:23:09] - Logon - Process Retcode 0

[2013-10-02 17:23:09] - Logon - 0: Closing now

[2013-10-02 17:23:09] - Logon - form Closing

[2013-10-02 17:23:09] - Logon - LogOn_Click concluded

[2013-10-02 17:23:09] - Logon - Done, verified user : [suser]

[2013-10-02 17:23:09] - VT_Service - Back from the Logon Form

[2013-10-02 17:23:09] - VT_Service - Making Menu Updates

[2013-10-02 17:23:09] - VT_Service - Call the Set Service Frorm

[2013-10-02 17:23:10] - SetService - Presenting form

[2013-10-02 17:23:10] - SetService - Cancel Clicked

[2013-10-02 17:23:10] - SetService - Closing form

[2013-10-02 17:23:09] - VT_Service - Main - form entered

[2013-10-02 17:23:09] - VT_Service - Main - Exit Click

[2013-10-02 17:23:09] - VT_Service closed

2013-10-02 17:23:09 - =====================================

多用户访问测试

我将XML引入我的测试框架的原因是为了测试不同的数据标准而无需重复代码。您可能会注意到,在文章前面,我注释掉了从XML文档中检索的第二个用户。但是,如上所述的代码将在尝试处理第二个用户时产生一个失败的测试。这是因为当在主菜单上单击“Exit”时,它会终止运行被测应用程序的进程,因此,创建与之关联的类的调用需要移到XML文档的导航中。

在C#实现中,我删除了PositveTestMethod中上述的“Using”子句,而Visual C++/CLI版本中创建VT_Service类实例的语句被移动了。现在,两个版本都将在加载用户名和密码的do-while循环之前实例化VT_Service类。

在C#中

    vtServ = new VTService(m_sw);                            
    vtServ.Clear();

在Visual C++/CLI中

	vtServ = gcnew VTService(m_sw);
	vtServ->Clear();

当我再次运行时,我可以看到两个用户都被应用了,并且框架日志也证实了这一点。然而,被测应用程序的日志表明,两次都应用了相同的用户。这是另一个可能发生在任何一方的错误。框架中可能存在一个初始化问题,我没有正确重置第二个用户,或者应用程序可能在使用列表中的第一个用户,而不管输入了什么。

这一次,我将手动运行被测应用程序以测试第二个用户并检查其日志。这证实了我的怀疑,即错误在被测应用程序中。您可以通过运行框架针对演示应用程序V2来重现此错误。

演示应用程序的V3已纠正此错误,并且日志已整理。

第二次测试

虽然为每个测试编写特定于案例的代码是有成本的,但其好处是在Test Explorer中为每个测试生成条目。如果我将XML文档结构化为允许我无需额外编码即可插入测试,那么将要付出的代价将是失去Test Explorer功能中的任何实际价值。

第二次测试的目标

第二次测试将登录并选择一个路线,然后访问主表单。

添加第二次测试

在对测试框架进行任何操作之前,我的第一个操作是在XML文档中创建一个名为ChooseServiceTest的新节点。起初,我只有登录凭据。

  <ChooseServiceTest>
    <TestRun1>
      <Username>suser</Username>
      <Password>spass</Password>
    </TestRun1>
    </ChooseServiceTest>

PositiveLogonTest之后立即添加以下代码

在C#中

 [TestMethod]
 public void ChooseServiceTest()
 {
 }

在Visual C++/CLI中

[TestMethod]
		void ChooseServiceTest()
		{
		}

我复制并重命名整个PositiveLogonTest可能差不多,因为XML导航和登录操作是相同的。区别将从“SetService”表单开始。所以接下来。我将复制第一个测试方法的正文到第二个。

我们将把取消设置服务的指令替换为新的测试逻辑,以选择一个服务并单击OK,而PositiveLogonTest点击了Cancel。

VTService类已经知道“SetService”表单上的两个按钮 - 但我现在必须告诉它关于用于路线选择的列表框和组合框,以及用于服务选择的列表框。路线选择列表框的构建风格类似于一个微调按钮。来自[StackOverflow]上另一位学习者的一个问题解锁了列表项,但并非没有问题。我开始向服务类添加三个新属性: 

在C#中

        private AutomationElement _RouteNoListBoxAutomationElement;
        private AutomationElementCollection _RouteNoListBoxItems;
        private AutomationElement _ItemToSelectInRouteNoListBox;

在Visual C++/CLI中

        private AutomationElement _RouteNoListBoxAutomationElement;
        private AutomationElementCollection _RouteNoListBoxItems;
        private AutomationElement _ItemToSelectInRouteNoListBox;

当SetService表单加载时,路线列表框及其构成元素也必须加载,因此将以下代码添加到VTSetServiceLaunched()方法中

在C#中

            _RouteNoListBoxAutomationElement = _VTSetServiceAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, "listRoute"));

            if (_RouteNoListBoxAutomationElement == null)
            {
                LogEntry("Could not find route list box");
                throw new InvalidOperationException("Could not find route list box");
            }
            //Load in the items of the route list box
            _RouteNoListBoxItems = _RouteNoListBoxAutomationElement.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ListItem));

在Visual C++/CLI中

			_RouteNoListBoxAutomationElement = _VTSetServiceAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::AutomationIdProperty, "listRoute"));

			if (_RouteNoListBoxAutomationElement == nullptr)
			{
				LogEntry("Could not find route list box");
				throw gcnew InvalidOperationException("Could not find route list box");
			}
			//Load in the items of the route list box
			_RouteNoListBoxItems = _RouteNoListBoxAutomationElement->FindAll(TreeScope::Children, gcnew PropertyCondition(AutomationElement::ControlTypeProperty, ControlType::ListItem));

现在我需要一个属性来选择SetService列表上的路线。我将其命名为RouteNo。它可以位于服务类中,在password属性之后,它的编码方式如下:

在C#中

    public object RouteNo
    {
        get
        {
            return _RouteNoListBoxAutomationElement.GetCurrentPropertyValue(AutomationElement.NameProperty);
        }
        set
        {
            if (_RouteNoListBoxAutomationElement != null)
            {
                try
                {
                    _ItemToSelectInRouteNoListBox = _RouteNoListBoxItems[System.Convert.ToInt32(value.ToString())];
                    Object selectPattern = null;

                    if (_ItemToSelectInRouteNoListBox.TryGetCurrentPattern(SelectionItemPattern.Pattern, out selectPattern))
                    {
                        (selectPattern as SelectionItemPattern).AddToSelection();
                        (selectPattern as SelectionItemPattern).Select();
                    } 
                    LogEntry("RouteNo value set to " + value.ToString());
                }
                catch (Exception e1)
                {
                    LogEntry("Error " + e1.Message + " setting RouteNo Value in " + _RouteNoListBoxAutomationElement.Current.AutomationId.ToString());
                    throw new InvalidOperationException("Error " + e1.Message + " setting RouteNo Value in " + _RouteNoListBoxAutomationElement.Current.AutomationId.ToString());
                }
            }
        }
    }

在Visual C++/CLI中

	property Object ^RouteNo
	{
		Object ^get()
		{
			return _RouteNoListBoxAutomationElement->GetCurrentPropertyValue(AutomationElement::NameProperty);
		}
		void set(Object ^value)
		{
			if (_RouteNoListBoxAutomationElement != nullptr)
			{
				try
				{
					_ItemToSelectInRouteNoListBox = _RouteNoListBoxItems[System::Convert::ToInt32(value->ToString())];
					Object ^selectPattern = nullptr;

					if (_ItemToSelectInRouteNoListBox->TryGetCurrentPattern(SelectionItemPattern::Pattern, selectPattern))
					{
						(dynamic_cast<SelectionItemPattern^>(selectPattern))->AddToSelection();
						(dynamic_cast<SelectionItemPattern^>(selectPattern))->Select();
					}
					LogEntry("RouteNo value set to " + value->ToString());
				}
				catch (Exception ^e1)
				{
					LogEntry("Error " + e1->Message + " setting RouteNo Value in " + _RouteNoListBoxAutomationElement->Current.AutomationId->ToString());
					throw gcnew InvalidOperationException("Error " + e1->Message + " setting RouteNo Value in " + _RouteNoListBoxAutomationElement->Current.AutomationId->ToString());
				}
			}
		}
	}

如果我现在在编译器上快速运行解决方案,Visual C++/CLI版本将给出上面提到的可怕的链接错误。但一旦我调用RouteNo属性,它们就会消失。

现在我回到ChooseServiceTest方法,在点击登录屏幕上的“OK”之后,我将硬编码选择路线1并点击SetService表单上的OK。一旦我对如何管理控件感到满意,这将被整合到XML中。因此,修改后的处理SetService的代码位于VTSetServiceLaunched调用之后:

在C#中

        vtServ.RouteNo = 1;
        vtServ.GetInvokePattern(vtServ.GetSetServiceFrmButton("btnOK")).Invoke();

在Visual C++/CLI中

        vtServ->RouteNo = 1;
        vtServ->GetInvokePattern(vtServ->GetSetServiceFrmButton("btnOK"))->Invoke();

现在,两个测试框架都正确选择了路线1,被测应用程序的日志也证实了这一点。然而,当手动运行应用程序时,对Route Listbox的更改(显示路线编号)会自动反映在显示路线名称的Route combo中。这并未发生,大致与[StackOverflow]帖子中报告的问题一致。我的初步怀疑指向了被测应用程序。让我们看看。

SetService上的ListRoutecmbRoute是协同工作的,但我没有明确的代码来实现这一点。当我将它们都绑定到SetService开发过程中的同一数据表时,我“免费”获得了这个功能。对SetService进行更广泛的手动测试,例如用鼠标指向设置listRoute并指向服务下一个,或从listRoute进行ShitTab操作,都不会填充cmbRoute,并且大多数时候会抛出异常。似乎由于没有处理listRoute的变化,我在SetService中存在一个错误。

目前,我将引入一种同情的测试。这通常不是个好主意,但在此案例中,我的目标是展示路线和服务的成功选择,因此我将在我的“实时”代码中回到生产框架中的这些错误。因此,鉴于路线名称和服务的组合框从自动化角度来看与列表框的处理方式相同,我将列表框选择的代码扩展到了RouteName Combo。被测应用程序会根据所选路线项的值来填充服务组合框。但什么都没发生!

第二个及后续的列表框或任何组合框,如果根据内部搜索结果来看,会给大多数初学MS UI Automation的开发者带来麻烦。尝试了包括展开/折叠在内的各种提示,但都没有成功。最终,我找到了一个涉及缓存组合框项的解决方案。

最终,这归结为缓存组合框内容并从缓存列表中选择所选条目。

首先,两个实例都获得了新的自动化元素_RouteNameComboAutomationElement,并且在VTService类中添加了两个新方法。第一个将组合框缓存到自动化元素中,第二个用于从缓存中选择。它们在此处显示,注释已删除以示简洁,包括注明我找到它们的位置。

在C#中

        AutomationElement CachePropertiesWithScope(AutomationElement elementMain)
        {
            AutomationElement elementList;

            CacheRequest cacheRequest = new CacheRequest();
            cacheRequest.Add(AutomationElement.NameProperty);
            cacheRequest.TreeScope = TreeScope.Element | TreeScope.Children;

            using (cacheRequest.Activate())
            {
                Condition cond = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.List);
                elementList = elementMain.FindFirst(TreeScope.Children, cond);
            }
            if (elementList == null) return null;

            foreach (AutomationElement listItem in elementList.CachedChildren)
            {
                LogEntry("Caching:" + listItem.Cached.Name);
            }

            AutomationElement child = elementList.CachedChildren[0];
            LogEntry("Caching child:" + child.CachedParent.Cached.Name);

            return elementList;
        }

        {
            SelectionItemPattern select = (SelectionItemPattern)element.GetCurrentPattern(SelectionItemPattern.Pattern);
            select.Select();
        }

在Visual C++/CLI中

		AutomationElement ^CachePropertiesWithScope(AutomationElement ^elementMain)
		{
			AutomationElement ^elementList;

			CacheRequest ^cacheRequest = gcnew CacheRequest();
			cacheRequest->Add(AutomationElement::NameProperty);
			cacheRequest->TreeScope = TreeScope::Element | TreeScope::Children;

			cacheRequest->Activate();
			try
			{
				Condition ^cond = gcnew PropertyCondition(AutomationElement::ControlTypeProperty, ControlType::List);
				elementList = elementMain->FindFirst(TreeScope::Children, cond);
			}
			finally
			{
			}
			if (elementList == nullptr)
				return nullptr;

			for each (AutomationElement ^listItem in elementList->CachedChildren)
			{
				LogEntry("Caching:" + listItem->Cached.Name);
			}

			AutomationElement ^child = elementList->CachedChildren[0];
			LogEntry("Caching child:" + child->CachedParent->Cached.Name);

			return elementList;
		}

		void Select(AutomationElement ^element)
		{
			SelectionItemPattern ^select = safe_cast<SelectionItemPattern^>(element->GetCurrentPattern(SelectionItemPattern::Pattern));
			select->Select();
		}

测试方法中的XML导航也需要更新。从现在开始,密码节点功能在单击“OK”并启动VTSetServiceLaunched之后就完成了。添加了一个新子句来处理XML中添加的“Route”节点。这是处理Route的代码

在C#中

     if (m_Navigator.Name == "Route")
    {
        m_Navigator.MoveToFirstChild();
        vtServ.RouteNo = System.Convert.ToInt32(m_Navigator.Value);
        vtServ.GetInvokePattern(vtServ.GetSetServiceFrmButton("btnOK")).Invoke();
        //Establish that the Main form is opened
        vtServ.VTMainLaunched();
        // Exit the Main form
        vtServ.GetInvokePattern(vtServ.GetMainFrmButton("Exit")).Invoke();
    }

在Visual C++/CLI中

	if (m_Navigator->Name == "Route")
	{
		m_Navigator->MoveToFirstChild();
		vtServ->RouteNo = System::Convert::ToInt32(m_Navigator->Value);
	    vtServ->GetInvokePattern(vtServ->GetSetServiceFrmButton("btnOK"))->Invoke();
		//Establish that the Main form is opened
		vtServ->VTMainLaunched();
		// Exit the Main form
		vtServ->GetInvokePattern(vtServ->GetMainFrmButton("Exit"))->Invoke();
	}

此测试的XML现在显示

  <ChooseServiceTest>
    <TestRun1>
      <Username>suser</Username>
      <Password>spass</Password>
      <Route>1</Route>
    </TestRun1>
  </ChooseServiceTest>

由于我选择在从路线列表框中选择路线编号后,从路线名称组合框中选择一个项目,这触发了路线列表框的离开事件,其一部分作用是为选定的路线填充服务组合框。如果我没有选择这样做,我将不得不找到另一种触发列表框离开事件的方法。一个选项是更改我的应用程序,以便在路线列表框的选择更改事件上加载服务,但我故意避免了这一点,因为我不希望它在用户选择“箭头”向下滚动列表时为每次选择更改而触发。

所以,我选择了我的路线,系统为我提供了该路线的服务列表。我将_ServiceComboAutomationElement添加到VTService类中用于处理此问题。我对VTService类的RouteNo属性添加了一些额外的代码,该代码将根据所选路线将服务列表缓存到其新的自动化元素中。代码如下:

在C#中

       _ServiceComboAutomationElement = CachePropertiesWithScope(_VTSetServiceAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, "cmbService")));

       if (_ServiceComboAutomationElement == null)
       {
             LogEntry("Could not find service combo box");
             throw new InvalidOperationException("Could not find service combo box");
       }

在Visual C++/CLI中

        _ServiceComboAutomationElement = CachePropertiesWithScope(_VTSetServiceAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::AutomationIdProperty, "cmbService")));

        if (_ServiceComboAutomationElement == nullptr)
        {
            LogEntry("Could not find service combo box");
            throw gcnew InvalidOperationException("Could not find service combo box");
        }

VTService类还需要一个新属性“ServiceNo”,它将处理XML中指定的服务设置。它可以位于RouteNo之后,形式如下:

在C#中

    public object ServiceNo
    {
       get
       {
          return _ServiceComboAutomationElement.GetCurrentPropertyValue(AutomationElement.NameProperty);
       }
       set
       {
           AutomationElement selectedServiceName = _ServiceComboAutomationElement.CachedChildren[System.Convert.ToInt32(value.ToString())];
           LogEntry("Service name set to " + selectedServiceName.Cached.Name);
           Select(selectedServiceName);
       }
    }

在Visual C++/CLI中

	property Object ^ServiceNo
	{
		Object ^get()
			{
				return _ServiceComboAutomationElement->GetCurrentPropertyValue(AutomationElement::NameProperty);
			}
			void set(Object ^value)
			{
				AutomationElement ^selectedServiceName = _ServiceComboAutomationElement->CachedChildren[System::Convert::ToInt32(value->ToString())];
				LogEntry("Service name set to " + selectedServiceName->Cached.Name);
				Select(selectedServiceName);
			}
	}

测试方法本身也需要更新以处理服务节点(请注意,这涉及简化路线节点的功能)

在C#中

    if (m_Navigator.Name == "Route")
    {
       m_Navigator.MoveToFirstChild();
       vtServ.RouteNo = System.Convert.ToInt32(m_Navigator.Value);
    }
    else
       if (m_Navigator.Name == "Service")
       {
          m_Navigator.MoveToFirstChild();
          vtServ.ServiceNo = System.Convert.ToInt32(m_Navigator.Value);
          vtServ.GetInvokePattern(vtServ.GetSetServiceFrmButton("btnOK")).Invoke();
          //Establish that the Main form is opened
          vtServ.VTMainLaunched();
          // Exit the Main form
          vtServ.GetInvokePattern(vtServ.GetMainFrmButton("Exit")).Invoke();
		}

在Visual C++/CLI中

	if (m_Navigator->Name == "Route")
	{
		m_Navigator->MoveToFirstChild();
		vtServ->RouteNo = System::Convert::ToInt32(m_Navigator->Value);
	}
	else
		if (m_Navigator->Name == "Service")
		{
			m_Navigator->MoveToFirstChild();
			vtServ->ServiceNo = System::Convert::ToInt32(m_Navigator->Value);
			vtServ->GetInvokePattern(vtServ->GetSetServiceFrmButton("btnOK"))->Invoke();
			//Establish that the Main form is opened
			vtServ->VTMainLaunched();
			// Exit the Main form
			vtServ->GetInvokePattern(vtServ->GetMainFrmButton("Exit"))->Invoke();
		}

XML也需要有一个Service节点。将其放在Route之后。

MenuStrip自动化

接下来,作为示例,是单击“Action”菜单并选择Show Settings。由于这只是一个演示,我没有将其包含在XML中,而是将其激活在离开服务选择菜单后立即单击“OK”。

菜单使用的自动化元素模式

点击Action菜单的初始尝试是通过精确复制[KAILA]计算器示例中点击Edit菜单的逻辑来完成的。对menuElement的广泛挖掘显示其程序化名称为“ExpandCollapsePatternIdentifiers.Pattern”,而我的名字是“InvokePatternIdentifiers.Pattern”。

为期六周的研究,从间歇性到深入。到了最后,我几乎要将命令按钮应用为我主应用程序桌面的“快捷方式”,以便在自动化测试期间绕过菜单。幸运的是,在我读到传奇人物James McCaffrey博士认为UI Automation可能无法与menuStrip控件一起使用的时候,我在CodeProject上找到了答案!它是我现在写的时候一篇不太受关注的文章(现为提示)的主题,作者是Varun Jain,(UI Automation Framework Interesting Challenge)。

根据我的阅读,ExpandCollapse适用于旧的MFC表单和新的WPF,因为它们使用实现了它的menuItem,但介于两者之间的变体,WindowsForms使用不实现ExpandCollapsemenuStrip,它使用InvokePattern。我保留了ExpandCollapse代码,以便在需要自动化合格表单的测试时使用。

那么,关于代码。

在测试方法中,用于测试XML中服务节点的代码被修改为打开**Actions**菜单并单击**Show Settings**作为其最后一个动作。在快速的机器上,此操作太快而无法观察,但它还会触发日志中的一个条目,以证明菜单操作和之前的选择。

在C#中

        //Establish that the Main form is opened
        vtServ.VTMainLaunched();
        vtServ.OpenMenu(VTService.VTServiceMenu.Actions);
        vtServ.ExecuteMenuByName("Show Settings");
        // Exit the Main form
        vtServ.GetInvokePattern(vtServ.GetMainFrmButton("Exit")).Invoke();

在Visual C++/CLI中

		//Establish that the Main form is opened
		vtServ->VTMainLaunched();
        vtServ->OpenMenu(VTService::VTServiceMenu::Actions);
        vtServ->ExecuteMenuByName("Show Settings");
		// Exit the Main form
		vtServ->GetInvokePattern(vtServ->GetMainFrmButton("Exit"))->Invoke();

在将任何方法添加到VTService类之前,它需要一个枚举类型来表示主菜单

在C#中

        public enum VTServiceMenu
        {
            Actions, //dataSetupToolStripMenuItem,
            Help
        }

在Visual C++/CLI中

        enum class VTServiceMenu
        {
            Actions, //dataSetupToolStripMenuItem,
            Help
        };

添加到VTService类以实现此目的的新方法是

在C#中

        public void OpenMenu(VTServiceMenu menu)
        {
            InvokePattern invPattern = GetInvokeMenuPattern(menu);
            invPattern.Invoke();
        }

        public InvokePattern GetInvokeMenuPattern(VTServiceMenu menu)
        {
            AutomationElement menuElement = _VTMainAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, menu.ToString()));
            AutomationPattern[] autoPattern;
            autoPattern = menuElement.GetSupportedPatterns();

            InvokePattern invPattern = menuElement.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            return invPattern;
        }
        public void ExecuteMenuByName(string menuName)
        {
            AutomationElement menuElement = _VTMainAutomationElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, menuName));
            if (menuElement == null)
            {
                return;
            }

            InvokePattern invokePattern = menuElement.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            if (invokePattern != null)
            {
                invokePattern.Invoke();
            }
        }

在Visual C++/CLI中

        void OpenMenu(VTServiceMenu ^menu)
        {
            InvokePattern ^invPattern = GetInvokeMenuPattern(menu);
            invPattern->Invoke();
        }

        InvokePattern ^GetInvokeMenuPattern(VTServiceMenu ^menu)
        {
            AutomationElement ^menuElement = _VTMainAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::NameProperty, System::Convert::ToString(menu)));
            array<AutomationPattern^> ^autoPattern;
            autoPattern = menuElement->GetSupportedPatterns();

            InvokePattern ^invPattern = dynamic_cast<InvokePattern^>(menuElement->GetCurrentPattern(InvokePattern::Pattern));
            return invPattern;
        }

        void ExecuteMenuByName(String^ menuName)
        {
            AutomationElement ^menuElement = _VTMainAutomationElement->FindFirst(TreeScope::Descendants, gcnew PropertyCondition(AutomationElement::NameProperty, menuName));
            if (menuElement == nullptr)
            {
                return;
            }

			InvokePattern ^invokePattern = dynamic_cast<InvokePattern^>(menuElement->GetCurrentPattern(InvokePattern::Pattern));
            if (invokePattern != nullptr)
            {
                invokePattern->Invoke();
            }
        }

结论

在编写本文时,我获得了足够的UI自动化测试知识,可以有效地将其部署到我的票务解决方案中。为了有效地做到这一点,我需要重新检查我解析XML的方式。在架构上,我还会考虑Nunit(来自WeDoQA)作为Visual Studio Test Explorer的替代方案,用于执行测试。我认为探索Excel作为驱动测试的手段也值得,命令和测试数据在不同的工作表中。然而,从开发通用便携式框架的角度来看,Excel会产生一笔许可费,而纯Visual Studio Express或VS Express + Nunit部署则没有这种费用。

功能上,我需要为复选框和单选组添加代码。我选择不在这里这样做,因为我需要设置一些范围边界,并且CodeProject上已经有关于它们的优秀文章。将来,我还需要自动化测试多网格表单上的网格控件。如果它成为一项重要的任务,我将把我在过程中做的笔记也写成一篇文章。

历史

2014-01-08 - V1.0 - 初始提交

© . All rights reserved.