UI 测试可以使用接口确定在运行时从哪些类执行代码
关于当前 UI 自动化解决方案如何工作的一些背景知识 -
我们的应用程序是 Windows WPF 应用程序,因此我们利用 WinAppDriver 来满足自动化测试需求。此解决方案与典型的 UI 自动化页面对象设计非常相似。我们有引用元素的页面对象,然后在测试中我们调用这些页面对象的方法来在主机上执行操作。页面对象使用 C# 分部类。一个类用于存储元素,一个类用于使用这些元素并执行操作。
测试类全部继承自处理 StartUp 和 TearDown 登录的 TestClassBase。因此,诸如登录页面和与之交互的测试类之类的当前设计看起来像这样
Login.Elements.cs
namespace UITesting
{
public partial class Login
{
public WindowsElement usernameField => _session.FindElementByAccessibilityId("UserName");
public WindowsElement passwordField => _session.FindElementByAccessibilityId("Password");
public WindowsElement signInButton => _session.FindElementByAccessibilityId("Sign In");
}
}
Login.Actions.cs
namespace UITesting
{
public partial class Login
{
// Driver Setup
private readonly WindowsDriver<WindowsElement> _session;
public Login(WindowsDriver<WindowsElement> session) => _session = session;
// Enter Username
public void EnterUsername(string username)
{
usernameField.SendKeys(username);
}
// Enter Password
public void EnterPassword(string password)
{
passwordField.SendKeys(password)
}
// Click 'Sign In'
public void SignIn()
{
signInButton.Click();
}
}
}
LoginTests.cs
namespace UITesting.Test
{
[Category("Login Tests")]
class LoginTests : TestClassBase
{
[Test]
public void Login()
{
// Login
login.EnterUsername("TestUser1");
login.EnterPassword("Password");
login.ClickSignIn();
}
}
}
TestClassBase
namespace UITesting
{
[TestFixture]
public class TestClassBase
{
// Declare Page Ogjects
public Login login;
// Declare WinAppDriver Session
private WindowsDriver<WindowsElement> session;
[SetUp]
public void SetUp()
{
// Instantiate Page Objects
login = new Login(session);
// Additional SetUp Logic here...
}
[TearDown]
public void TearDown()
{
// TearDown Logic here...
}
}
}
这一切都运行得很好,很棒,但我想做的是将其发展为可以在不同主机上使用相同代码运行完全相同的测试。
我们还有一个利用 Uno 平台的应用程序的网络版本。该应用程序与网络上的应用程序几乎相同,但为了实现自动化,我们需要使用 Selenium。我不想做的是必须管理两个单独的 UI 自动化解决方案,并且由于应用程序的两个版本几乎相同,我希望能够切换在我们的 CI 中运行测试的目标平台/CD 管道,这最终将改变正在执行的代码。
所以看来利用接口可能是这里的方法,而且我知道使用它们现在可以拥有如下所示的页面对象类结构
ILogin.cs
LoginWeb.Actions.cs
LoginWeb.Elements.cs
LoginWPF.Actions.cs
LoginWPF.Elements.cs
这样,我现在有 4 个部分类,其中 Actions 类继承接口并且它们使用相应 Elements 类中的元素。
我不明白的部分是如何让测试类现在执行所需 Actions 类中的代码。我实例化页面对象的部分是关键,因为在此示例中,WPF 和网页对象都需要共享名称登录。我是否必须为它们创建两个不同的 TestClassBase 类和某种接口,并让测试继承这两个类?或者我只是以完全错误的方式处理这个问题..
So just some background on how the current UI automation solution works -
Our application is a Windows WPF app, so we utilize WinAppDriver for our automated testing needs. The solution for this is very similar to your typical UI automation page object design. We have page objects that reference elements, and then in our tests we call the methods from these page objects to perform actions on the host. The page objects make use of the C# partial classes. One class to store elements, one class to use these elements and perform actions
The test classes all inherit from a TestClassBase that handles the StartUp and TearDown login. So current design for something like a Login page and a test class that interacts with it looks like this
Login.Elements.cs
namespace UITesting
{
public partial class Login
{
public WindowsElement usernameField => _session.FindElementByAccessibilityId("UserName");
public WindowsElement passwordField => _session.FindElementByAccessibilityId("Password");
public WindowsElement signInButton => _session.FindElementByAccessibilityId("Sign In");
}
}
Login.Actions.cs
namespace UITesting
{
public partial class Login
{
// Driver Setup
private readonly WindowsDriver<WindowsElement> _session;
public Login(WindowsDriver<WindowsElement> session) => _session = session;
// Enter Username
public void EnterUsername(string username)
{
usernameField.SendKeys(username);
}
// Enter Password
public void EnterPassword(string password)
{
passwordField.SendKeys(password)
}
// Click 'Sign In'
public void SignIn()
{
signInButton.Click();
}
}
}
LoginTests.cs
namespace UITesting.Test
{
[Category("Login Tests")]
class LoginTests : TestClassBase
{
[Test]
public void Login()
{
// Login
login.EnterUsername("TestUser1");
login.EnterPassword("Password");
login.ClickSignIn();
}
}
}
TestClassBase
namespace UITesting
{
[TestFixture]
public class TestClassBase
{
// Declare Page Ogjects
public Login login;
// Declare WinAppDriver Session
private WindowsDriver<WindowsElement> session;
[SetUp]
public void SetUp()
{
// Instantiate Page Objects
login = new Login(session);
// Additional SetUp Logic here...
}
[TearDown]
public void TearDown()
{
// TearDown Logic here...
}
}
}
This all works well and great, but what I am trying to do is evolve this into is something that can run the exact same test using the same code on a different host.
We also have a Web version of the app that utilizes the Uno platform. The app is pretty much identical on the web, but to automate it we need to use Selenium. What I don't want to do is to have to manage two separate UI automation solutions, and since the two versions of the app are pretty much identical, I want to be able to toggle the target platform that the tests run on in our CI/CD pipeline and this will ultimately change what code is getting executed.
So it seems like utilizing Interfaces is probably the way to go here, and I understand that using them it would be possible to now have a Page Object class structure like below
ILogin.cs
LoginWeb.Actions.cs
LoginWeb.Elements.cs
LoginWPF.Actions.cs
LoginWPF.Elements.cs
This way, I now have 4 partial classes where the Actions classes inherit the interface and they use the elements from their corresponding Elements class.
The part that I don't understand is how I can get the test class to now execute the code from the desired Actions class. The part where I instantiate the page objects is key, as in this example both the WPF and Web page object would need to share the name login. Would I have to create two different TestClassBase classes and some sort of Interface for them and have the tests inherit both? Or am I just going about this the completely wrong way..
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
这可能是一项更大的重构工作,但值得付出努力。
首先,您需要为每个页面模型创建界面。我建议保持接口尽可能简单,以提供完整且灵活的抽象。考虑使用一个名为
SignIn
的方法,该方法接受用户名和密码作为参数,而不是必须按特定顺序调用三个单独的方法(EnterUsername、EnterPassword 和 ClickSignIn)。该方法将在内部处理输入用户名、密码并单击相应的按钮。确实,如果你走这条路,请仔细考虑接口。尽量避免任何调用 order 方法的情况。尝试关注用例,而不是满足该用例所需的步骤。
接下来,在两个不同的类上实现该接口。每个课程都将专门研究 Selenium 或 WinAppDriver。考虑使用命名约定,其中处理 Web 应用程序的页面模型以“Web”为前缀,而桌面应用程序的页面模型以“Windows”或“桌面”为前缀。
一旦有了适当的抽象,您将需要一个用于创建页面模型的工厂类的接口,然后是两个实现类:
这是 抽象工厂模式,是一种无需诉诸类反射即可采用的方法。虽然类反射可能需要更少的代码,但它更难以理解。只是为了搞笑,这里尝试使用类反射来生成页面模型:
您可以将它与任一驱动程序一起使用:
除非您或您的团队对类反射感到满意,否则我会推荐抽象工厂模式方法,因为它更容易理解。
无论哪种方式,您都需要确定您正在使用哪个客户端(Web 与桌面)。这应该在测试的设置方法中完成。建议将您的测试重构为基类以集中此决策代码。
This might be a larger refactoring job, but it will be worth the effort.
First, you'll need to create interfaces for each page model. I recommend keeping the interfaces as simple as possible in order to provide a complete and flexible abstraction. Instead of three separate methods (EnterUsername, EnterPassword and ClickSignIn) which must be called in a specific order, consider a single method called
SignIn
which accepts a username and password as arguments. The method will internally handle entering the username, password and clicking the appropriate button.Really, if you go this route, think hard about the interfaces. Try to avoid any situation where the order methods are called matters. Try to focus on the use case, and not the steps required to satisfy that use case.
Next, implement this interface on two different classes. Each class will specialize in Selenium or WinAppDriver. Consider using a naming convention where page models that deal with the web application are prefixed with "Web" and page models for the desktop app are prefixed with "Windows" or "Desktop".
Once you have a proper abstraction, you will need an interface for a factory class that creates page models, and then two implementing classes:
This is an implementation of the Abstract Factory Pattern, and is an approach you can take without resorting to class reflection. While class reflection would probably take less code, it is much more difficult to understand. Just for giggles, here is an attempt at class reflection to generate page models:
You can use it with either driver:
Unless you or your team are comfortable with class reflection, I would recommend the abstract factory pattern approach, just because it is easier to understand.
Either way, you will need to determine which client you are using (web versus desktop). This should be done in a the setup method for your test. Refactoring your tests into a base class to centralize this decision making code is advised.