框架 - 您的第一个框架 - 第 4 部分





5.00/5 (2投票s)
您的第一个框架。
在第三部分中,我们为框架添加了一些核心功能,例如窗口处理和 Dispose 方法,以便完成后进行清理。我们还研究了添加一些基本的 JavaScript 功能来调用页面上的脚本。到目前为止,我们已经有了框架的基础,如果我们愿意,可以将其直接用于编写测试和拆卸。但是,为 Selenium 和 WebDriver 创建框架应该是为了让您在编写测试时尽可能简化生活,其中一种方法是将现有的 WebDriver 功能包装到易于使用的帮助库中。
因此,在第四部分,我们将专门做这件事,并研究编写一些有用的代码,使我们与页面上的元素交互的方式更加轻松和用户友好。
在编写 Web 测试时,您会遇到的元素通常是文本框、按钮、复选框、下拉列表/选择列表或单选按钮。虽然肯定还有更多,但这些是您最常需要处理的,尤其是在学习 Selenium 的早期阶段。尽管这些元素在我们要测试的网页中有不同的交互方式,但对于 WebDriver 来说,它们可以被认为是非常相似的。它们都有一个我们需要找到的定位器,并且在测试中使用它们时,它们都具有与之相关的通用行为,例如测试它们的可见性、读取任何文本值或点击它们。
由于这些共同的属性,我们将创建一个接口作为通用元素对象的基类,并创建继承自该接口的特定于元素类型的类。为什么要为此使用接口?嗯,可以将接口想象成在主元素对象(接口)和使用该主元素的所有特定元素类型之间充当合同。它确保了每个特定于元素的类都实现了通用的行为和属性。
让我们从查看我们的接口 `IPageField` 开始
using OpenQA.Selenium;
namespace AutomationFramework.Engine.Controls
{
public interface IPageField<T>
{
string Name { get; }
By FindBy { get; }
T Get();
void Set(T value);
bool Exists();
bool IsVisible();
}
}
它并不复杂,但可能会有一些代码,如果您是编程和 C# 的新手,可能不完全理解。特别是 `IPageField<T>` 的 `<T>` 部分。这被称为泛型类型参数。泛型类型参数允许您在编译时为方法指定任意类型 `T`,而无需在方法或类声明中指定具体类型。在这种情况下,我们使用它的原因是我们要使用的类型将取决于我们正在创建的元素。文本框将使用 `string` 类型,而 `checkbox` 将使用 `bool` 类型,因为它只处理 `true` 或 `false`。当您查看“`T Get();`”这一行时,这再次说明 `Get` 方法的返回类型将由我们调用/创建的元素类型决定。
我们希望每个元素都具有的通用方法是使用该元素特有的类型进行 `Get` 和 `Set`,以及 `Exists` 或 `IsVisible`。同时拥有这两个很重要,因为虽然我们可能期望某物存在,但它并不一定意味着它总是可见的,所以处理这两种情况的能力很重要。
除此之外,它真的很简单,对吧?一点也不吓人。除了定义我们希望在元素之间使用的属性和方法之外,我们不需要在这个接口中做任何其他事情。
正如之前在文章中提到的,我们将为更常见的元素创建类。`Textbox`、`button`、`checkbox`、`radio` 和 `picklist`。
using OpenQA.Selenium;
using System;
namespace AutomationFramework.Engine.Controls
{
public class Checkbox : IPageField<bool>
{
public string Name { get; }
public By FindBy { get; }
public Checkbox(string name, By findBy)
{
Name = name;
FindBy = findBy;
}
public virtual bool Get()
{
if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()}
not found or is not visible"); };
var element = FindBy.FindElement();
return element.Selected;
}
public virtual void Set(bool value)
{
if(value != FindBy.WaitUntilElementIsVisible().Selected)
{
FindBy.WaitAndClickElement();
}
}
public virtual bool Exists() => FindBy.DoesElementExist();
public virtual bool IsVisible() => FindBy.IsElementVisible();
}
}
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
namespace AutomationFramework.Engine.Controls
{
public class Picklist : IPageField<string>
{
public string Name { get; }
public By FindBy { get; }
public Picklist(string name, By findBy)
{
Name = name;
FindBy = findBy;
}
public virtual string Get()
{
if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()}
not found or is not visible"); };
var element = FindBy.FindElement();
return element.Text;
}
public virtual void Set(string value)
{
FindBy.WaitUntilElementIsVisible();
FindBy.FindElement().Clear();
FindBy.FindElement().SendKeys(value);
}
public virtual void SetPicklistItemValue(string value)
{
if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()}
not found or is not visible"); };
var element = FindBy.FindElement();
if(ValidateItemExistsInPicklist(element, value))
{
new SelectElement(element).SelectByValue(value);
}
}
public virtual void SetPicklistItemText(string text)
{
if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()}
not found or is not visible"); };
var element = FindBy.FindElement();
if (ValidateItemExistsInPicklist(element, text))
{
new SelectElement(element).SelectByText(text);
}
}
public virtual bool ValidateItemExistsInPicklist(IWebElement element, string value)
{
var values = element.Text;
if(!values.Contains(value))
{
return false;
throw new Exception($"{value} not found in picklist");
}
return true;
}
public virtual bool Exists() => FindBy.DoesElementExist();
public virtual bool IsVisible() => FindBy.IsElementVisible();
}
}
using OpenQA.Selenium;
using System;
namespace AutomationFramework.Engine.Controls
{
public class Radio : IPageField<bool>
{
public string Name { get; }
public By FindBy { get; }
public Radio(string name, By findBy)
{
Name = name;
FindBy = findBy;
}
public virtual bool Get()
{
if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()}
not found or is not visible"); };
var element = FindBy.FindElement();
return element.Selected;
}
public virtual void Set(bool value)
{
if (value != FindBy.WaitUntilElementIsVisible().Selected)
{
FindBy.WaitAndClickElement();
}
}
public virtual bool Exists() => FindBy.DoesElementExist();
public virtual bool IsVisible() => FindBy.IsElementVisible();
}
}
using OpenQA.Selenium;
using System;
using System.Globalization;
namespace AutomationFramework.Engine.Controls
{
public class Text : IPageField<string>
{
public string Name { get; }
public By FindBy { get; }
public Text(string name, By findBy)
{
Name = name;
FindBy = findBy;
}
public virtual string Get()
{
if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()}
not found or is not visible"); };
var element = FindBy.FindElement();
return element.Text;
}
public virtual void Set(string value)
{
FindBy.WaitUntilElementIsVisible();
FindBy.FindElement().Clear();
FindBy.FindElement().SendKeys(value);
}
public virtual void Set(double value) => Set(value.ToString(CultureInfo.InvariantCulture));
public virtual void Clear() => FindBy.FindElement().Clear();
public virtual bool Exists() => FindBy.DoesElementExist();
public virtual bool IsVisible() => FindBy.IsElementVisible();
}
}
我们的每个类都有一个构造函数,它接受两个参数。名称作为 `string`,以及一个 `By` 对象,这就是我们的定位器。名称完全是可选的,只是为了提高可读性,理论上,您可以通过强制执行变量的强命名约定来消除它。然而,`By findBy` 是绝对必需的,它将用于查找我们的元素(使用 `ID`、`Xpath`、CSS 选择器等)。
正如您所见,以上所有类都共享我们接口中定义的属性和方法,它们至少必须这样做。但这并不意味着它们不能包含额外的功能,正如我们在 `picklist` 类中所展示的。我们实现了许多特定于该元素类型的方法,但其他元素类型无法使用它们。
为了真正体会到这种额外努力的价值,而不是仅仅将所有元素声明为 `IWebElements`,我们应该看看代码中是如何使用的。使用上述元素的示例页面对象类可能如下所示
using OpenQA.Selenium;
using AutomationFramework.Engine.Controls;
namespace AutomationFramework.Web.Tests.Pages
{
public static class LoginPage
{
public static Button LoginButton = new Button();
public static Button ForgotPassword = new Button();
public static Text UsernameInput = new Text("UsernameInput", By.Id("username"));
public static Text PasswordInput = new Text("PasswordInput", By.Id("password"));
public static Picklist UserType = new Picklist("UserType", By.Id("usertype"));
public static Checkbox TermsAndConditions = new Checkbox("TermsAndConditions", By.Id("tandc"));
}
}
在这里,您可以看到,我们创建页面上的每个新元素,我们都会创建一个我们想要交互的元素类型的对象。这使得您的页面对象类非常易于理解,并且在离开很多个月后返回维护时也更容易,因为您可以一目了然地看到构成页面的对象。如果不这样做,您将以以下方式指定每个元素
public static IWebElement LoginButton = Driver.FindElement(By.Id("LoginButton"));
与我们的 `Button` 对象相比,查看该示例中的 `LoginButton`,从可读性的角度来看,使用我们的 `Button` 对象要好得多。然而,从可用性的角度来看,它也更好得多。回到我们的 `picklist` 类,我们指定了该元素类型特有的方法,这意味着只有 `picklist` 对象可以使用它们。如果我们尝试从 `Button` 对象调用它们,即使它们都继承自同一个接口,`button` 对象也无法访问那些独有的 `picklist` 方法。这可以防止在尝试以不正确的方式与元素交互时出现粗心大意的代码或错误,并允许您或其他用户更有信心地创建代码,从而能够准确地知道他们如何使用每种对象类型。
这就到了第四部分的结尾。虽然我们添加的这些功能在框架中并非必需,但它改善了我们将要编写测试和创建页面对象类的方式。如果您以前没有见过接口,这也是对使用接口的一个很好的介绍。
下次,我们将研究为 Selenium WebDriver 添加一些功能,并创建一些扩展方法。扩展方法允许我们在不创建新派生类型、重新编译或修改原始类型的情况下,向现有类型添加方法。因此,在本例中,将向现有的 `WebDriver` 类型添加方法,并且在这种情况下,将改进我们查找和等待元素的方法。
帖子 Framework – Your First Framework – Part 4 最初出现在 Learn Automation。