UI 可以使用接口确定在运行时从哪个 类 执行代码的测试

UI Test that can determine which classes to execute code from at runtime using interfaces

所以只是一些关于当前 UI 自动化解决方案如何工作的背景知识 -

我们的应用程序是一个 Windows WPF 应用程序,因此我们利用 WinAppDriver 来满足我们的自动化测试需求。此解决方案与典型的 UI 自动化页面对象设计非常相似。我们有引用元素的页面对象,然后在我们的测试中我们调用这些页面对象的方法以在主机上执行操作。页面对象使用 C# 部分 classes。一个 class 用于存储元素,一个 class 用于使用这些元素并执行操作

测试 class 全部继承自处理 StartUp 和 TearDown 登录的 TestClassBase。因此,当前设计的登录页面和与之交互的测试 class 看起来像这样

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 平台的网络版应用程序。该应用程序与 Web 上的应用程序几乎相同,但要使其自动化,我们需要使用 Selenium。我不想做的是必须管理两个单独的 UI 自动化解决方案,并且由于应用程序的两个版本几乎相同,我希望能够切换测试的目标平台 运行 在我们的 CI/CD 管道中,这将最终改变正在执行的代码。

所以看起来利用接口可能是解决问题的方法,而且我知道使用它们现在可以拥有如下所示的页面对象 class 结构

ILogin.cs  
LoginWeb.Actions.cs 
LoginWeb.Elements.cs 
LoginWPF.Actions.cs  
LoginWPF.Elements.cs

通过这种方式,我现在有 4 个部分 classes,其中 Actions classes 继承了界面,并且它们使用了相应元素 class.[=17= 中的元素]

我不明白的部分是如何让测试 class 现在从所需的操作 class 执行代码。我实例化页面对象的部分是关键,因为在此示例中,WPF 和网页对象都需要共享名称登录。我是否必须为它们创建两个不同的 TestClassBase classes 和某种接口,并让测试继承两者?还是我只是以完全错误的方式解决这个问题..

这可能是一项更大的重构工作,但值得付出努力。

首先,您需要为每个页面模型创建界面。我建议保持接口尽可能简单,以提供完整和灵活的抽象。代替必须以特定顺序调用的三个单独的方法(EnterUsername、EnterPassword 和 ClickSignIn),考虑一个名为 SignIn 的方法,它接受用户名和密码作为参数。该方法将在内部处理输入用户名、密码并单击适当的按钮。

真的,如果你走这条路,请认真考虑接口。尽量避免调用排序方法的任何情况。尽量关注用例,而不是满足该用例所需的步骤。

public interface ILoginPage
{
    void SignIn(string username, string password);
}

接下来,在两个不同的classes 上实现这个接口。每个 class 将专注于 Selenium 或 WinAppDriver。考虑使用命名约定,其中处理 Web 应用程序的页面模型以“Web”为前缀,桌面应用程序的页面模型以“Windows”或“Desktop”为前缀。

public class WebLoginPage : ILoginPage
{
    private readonly IWebDriver driver;

    public WebLoginPage(IWebDriver driver)
    {
        this.driver = driver;
    }

    public void SignIn(string username, string password)
    {
        // Enter username
        // Enter password
        // Click sign-in button
    }
}

public class DesktopLoginPage : ILoginPage
{
    private readonly WindowsDriver<WindowsElement> session;

    public DesktopLoginPage (WindowsDriver<WindowsElement> session)
    {
        this.session = session;
    }

    public void SignIn(string username, string password)
    {
        // Enter username
        // Enter password
        // Click sign-in button
    }
}

一旦有了适当的抽象,您将需要一个用于创建页面模型的工厂 class 的接口,然后是两个实现 classes:

public interface IPageModelFactory
{
    ILoginPage CreateLoginPage();
}

public class WebPageModelFactory : IPageModelFactory
{
    private readonly IWebDriver driver;

    public PageModelFactory(IWebDriver driver)
    {
        this.driver = driver;
    }

    public ILoginPage CreateLoginPage()
    {
        return new WebLoginPage(driver);
    }
}

public class DesktopPageModelFactory : IPageModelFactory
{
    private readonly WindowsDriver<WindowsElement> session;

    public DesktopPageModelFactory(WindowsDriver<WindowsElement> session)
    {
        this.session = session;
    }

    public ILoginPage CreateLoginPage()
    {
        return new DesktopLoginPage(session);
    }
}

这是 Abstract Factory Pattern 的一个实现,是一种无需诉诸 class 反射即可采用的方法。虽然 class 反射可能需要更少的代码,但它更难理解。只是为了笑,这里是class反射生成页面模型的尝试:

public class PageModelFactory
{
    private readonly object client;

    public PageModelFactory(object client)
    {
        this.client = client;
    }

    public ILoginPage CreateLoginPage()
    {
        var pageModelType = GetPageModelType<ILoginPage>();
        var constructor = pageModelType.GetConstructor(new Type[] { client.GetType() });

        return (ILoginPage)constructor.Invoke(new object[] { client });
    }

    private Type GetPageModelType<TPageModelInterface>()
    {
        return client.GetType()
                     .Assembly
                     .GetTypes()
                     .Single(type => type.IsClass && typeof(TPageModelInterface).IsAssignableFrom(type));
    }
}

您可以将它与任一驱动程序一起使用:

// Selenium
var driver = new ChromeDriver();

// WinApDriver (however you initialize it)
var session = new WindowsDriver<WindowsElement>();

PageModelFactory webPages = new PageModelFactory(driver);
PageModelFactory desktopPages = new PageModelFactory(session);
ILoginPage loginPage = null;

loginPage = webPages .CreateLoginPage();
loginPage.SignIn("user", "...");

loginPage = desktopPages.CreateLoginPage();
loginPage.SignIn("user", "...");

除非您或您的团队对 class 反射感到满意,否则我会推荐抽象工厂模式方法,因为它更容易理解。

无论哪种方式,您都需要确定您使用的是哪个客户端(网络还是桌面)。这应该在测试的设置方法中完成。建议将您的测试重构为基础 class 以集中此决策代码。