页面对象模型的革命还是演变?

大家好! 我叫Artyom Sokovets 。 我想分享我有关Atlas的文章的译文:HTML Elements框架的转世,它提出了一种与Page Object完全不同的方法。

在详细介绍之前,我想问:您知道Page Object的包装多少? 页面元素,ScreenPlay,可加载组件,调用链...

如果我们在接口上实现一个页面对象,并固定代理模式并添加一些Java 8功能,将会发生什么?

如果有兴趣,我建议改用cat。



引言


使用标准PageObject设计模式时,会出现许多问题:

元素重复

public class MainPage { @FindBy(xpath = ".//div[@class = 'header']") private Header header; } public class AnyOtherPage { @FindBy(xpath = ".//div[@class = 'header']") private Header header; } 

在这里,Header块用于各种PageObject类。

元素缺乏参数化

 public class EditUserPage { @FindBy(xpath = "//div[text()='Text_1']") private TextBlock lastActivity; @FindBy(xpath = "//div[text()='Text_2']") private TextBlock blockReason; } 

本示例描述了用户设置编辑页面的元素。 两个TextBlock元素包含一个几乎相同的定位符,仅在文本值(“ Text_1”和“ Text_2”)上有所不同。

相同类型的代码

 public class UserPage { @FindBy(xpath = "//div[text()='']/input") private UfsTextInput innerPhone; @FindBy(xpath = "//div[text()='Email']/input") private UfsTextInput email; @FindBy(xpath = "//button[text()='']") private UfsButton save; @FindBy(xpath = "//button[text()='']") private UfsButton toUsersList; } 

在日常工作中,您可以找到Page Object,它由具有相同元素的多行代码组成。 将来,此类类难以维护。

大型课程

 public class MainSteps { public void hasText(HtmlElement e, Matcher m) public void hasValue(HtmlElement e, Matcher m) public void linkContains(HtmlElement e, String s) public void hasSize(List<HtmlElement> e, Matcher m) public void hasItem(List<HtmlElement> e, Matcher m) //... } 

随着时间的流逝,处理元素的步骤类别不断增长。 需要更多的注意,以便没有重复的方法。

您的Page Object世界指南


HTML Elements框架的转世旨在解决上述问题,减少测试项目的代码行数,更周到地处理列表和期望,并借助扩展系统为您自己微调该工具。

Atlas是新一代Java框架,用于通过接口实现Page Object模式的实现来开发UI自动测试。 这种方法提供了在元素树的构造中进行多重继承的可能性,这最终为您的自动测试提供了简洁的代码。

该框架的主要创新是使用接口而不是标准类。

这是github.com主页描述的样子:

 public interface MainPage extends WebPage, WithHeader { @FindBy("//a[contains(text(), 'Or start a free)]") AtlasWebElement trial(); } 

上面的代码描述了GitHub站点的主页,该页面具有一个元素以及从WebPage和WithHeader层的多个继承(该示例仅出于教育目的,因此省略了大多数Web元素)。

框架架构


Atlas当前包含三个模块:

  • 核心图集
  • Atlas-WebDriver
  • 阿特拉斯-阿皮

地图集核心介绍了使用接口处理页面对象的基本功能。 使用接口的想法来自著名的Retrofit工具。



其他两个atlas-webdriveratlas-appium模块用于开发自动化的UI Web和UI移动脚本。 描述网页的主要入口点是WebPage界面,而对于移动屏幕-屏幕。 从概念上讲,atlas-webdriver和atlas-appium建立在扩展名上(软件包* .extension)。

物品




该工具带有两个用于处理UI元素的专门类(类似于WebElement类)。



AtlasWebElementAtlasMobileElementshouldwaitUntil方法补充。 (本文将进一步讨论这些方法)。

该工具提供了通过扩展上述类来创建自己的组件的功能,从而使您可以创建跨平台元素。

主要特点


让我们更详细地考虑该工具的功能:



接口而不是类

描述标准PageObjects时,使用接口代替类。

 public interface MainPage extends WebPage, WithHeader { @FindBy("//a[contains(text(), 'Or start a free trial of Enterprise Server')]") AtlasWebElement trial(); } 

此示例描述了GitHub起始页面上的链接。

元素的参数化

想象一下,我们有一个带有字段的表格:



要对其进行描述,您需要使用@FindBy批注创建11个变量,并在必要时声明getter。

使用Atlas,您只需要一个参数化的AtlasWebElement元素。

 public interface MainPage extends WebPage { @FindBy("//div[text()='{{ text }}']/input") AtlasWebElement input(@Param("text") String text); } 

自动化测试代码如下:

 @Test public void simpleTest() { onMainPage().input("First Name").sendKeys("*"); onMainPage().input("Postcode").sendKeys("*"); onMainPage().input("Email").sendKeys("*"); } 

我们转到所需的页面,使用参数调用方法,然后使用元素执行所需的操作。 具有参数的方法描述特定字段。

多重继承

前面提到过,在不同页面对象中使用的块(例如Header)是代码的重复。

有一个标题github。



我们描述了此块(省略了大多数Web元素):

 public interface Header extends AtlasWebElement { @FindBy(".//input[contains(@class,'header-search-input')]") AtlasWebElement searchInput(); } 

接下来,创建一个可以连接到任何页面的图层:

 public interface WithHeader { @FindBy("//header[contains(@class,'Header')]") Header header(); } 

我们用标题块扩展主页。

 public interface MainPage extends WebPage, WithHeader { @FindBy("//a[contains(text(), 'Or start a)]") AtlasWebElement trial(); } 

通常,您可以创建更多层并将它们连接到所需页面。 在下面的示例中,从主页连接页眉,页脚,侧边栏层。

 public interface MainPage extends WithHeader, WithFooter, WithSidebar {} 

让我们继续前进。 页眉包含4个按钮,3个下拉菜单和一个搜索字段:



让我们创建自己的Button元素,并用一个元素描述四个按钮。

 public interface Button extends AtlasWebElement { @FindBy(".//a[contains(., '{{ value }}')]") AtlasWebElement selectButton(@Param("value") String value); } 

将按钮按钮连接到标题层。 因此,我们扩展了GitHub标头的功能。

 public interface Header extends WithButton { … } 

可以将单独的Button元素连接到网站的各个层,并快速在所需页面上获得所需元素。

一个例子:

 @Test public void simpleTest() { onMainPage().open("https://github.com"); onMainPage().header().button("Priing").click(); } 

在测试的第二行中,访问站点的标题,然后我们将调用参数化的按钮,其值为“ Pricing”,然后单击。

在经过测试的网站上,可能有很多元素在页面之间重复。 为了不使用标准的页面对象方法来全部描述它们,您可以描述一次并在必要时进行连接。 节省时间和代码行数是显而易见的。



默认方法

Java 8引入了默认方法,这些方法用于预定义所需的功能。

假设我们有一个“有害”元素:例如,一个复选框处于打开或关闭状态。 许多场景都通过了它。 如果禁用该复选框,则需要启用它:

 if(onMainPage().rentFilter().checkbox("").getAttribute("class").contains("disabled")) { onMainPage().rentFilter().checkbox("").click(); } 

为了不将所有这些代码存储在step类中,可以将其作为默认方法放在元素旁边。

 public interface Checkbox extends AtlasWebElement { @FindBy("//...") AtlasWebElement checkBox((@Param("value") String value); default void selectCheckbox(String value) { if (checkBox(value).getAttribute("class").contains("disabled")) { checkBox(value).click(); } } } 

现在测试步骤将如下所示:

 onMainPage().rentFilter().selectCheckbox(""); 

您要在字段中结合使用清理和键入字符的另一个示例。

 onMainPage().header().input("GitHub").clear(); onMainPage().header().input("GitHub").sendKeys("*"); 

定义一个清除字段并返回元素本身的方法:

 public interface Input extends AtlasWebElement { @FindBy("//xpath") AtlasWebElement input(@Param("value") String value); default AtlasWebElement withClearInput(String value) { input(value).clear(); return input(value); } } 

在测试方法中,步骤如下:

 onMainPage().header().withClearInput("GitHub").sendKeys("Atlas"); 

这样,可以对元素中的所需行为进行编程。

重试

Atlas具有内置重试功能。 您无需担心诸如NotFoundExceptionStaleElementReferenceExceptionWebDriverException之类的异常,并且还可以省去使用Selenium API的显式和隐式期望。

 onSite().onSearchPage("Junit 5").repositories().waitUntil(hasSize(10)); 

如果您在链中的某个点捕获到异常,则该阶段将从头开始重复。

可以独立调整可重复的时间间隔或重复频率。

 Atlas atlas = new Atlas(new WebDriverConfiguration(driver)) .context(new RetryerContext(new DefaultRetryer(3000L, 1000L, Collections.singletonList(Throwable.class)))); 

我们期望三秒钟的轮询速率为每秒一次。

我们还可以使用Retry注释来调整对特定项目的等待。 对于所有元素,搜索将在3秒内进行,如果搜索为1,则搜索为20。

 @Retry(timeout = 20_000L, polling = 2000L) @IOSFindBy(xpath = "//XCUIElementTypeSearchField[@name='Search Wikipedia']") @AndroidFindBy(xpath = "//*[contains(@text, 'Search Wikipedia')]") AtlasMobileElement searchWikipedia(); 

使用清单

开箱即用,该工具提供了使用列表的功能。 这是什么意思? 有一个带有输入标签的字段,我们在其中输入文本,然后出现一个下拉列表,元素不会立即出现。



对于这种情况,存在一个实体ElementsCollection。 在它的帮助下,可以使用列表。

 public interface ContributorsPage extends WebPage, WithHeader { @FindBy(".//ol[contains(@class, 'contrib-data')]//li[contains(@class, 'contrib-person')]") ElementsCollection<RepositoryCard> hovercards(); } 

用法:

 onSite().onContributorsPage().hovercards().waitUntil(hasSize(4)); 

也可以过滤元素并将其转换为其他类型的列表。

智能断言

如前所述, AtlasWebElementAtlasMobileElement实体使用should,waitUntil方法来处理检查(语句)。

方法内容描述
应该(...)使用一个元素执行各种语句(检查)。 引发AssertionError
waitUntil(...)使用一个元素执行各种语句(检查)。 引发RuntimeException

为什么要这样做? 为了节省分析自动化脚本的运行报告时的时间。 大多数功能检查都在脚本的末尾执行:功能测试专家会感兴趣,而自动化测试专家会进行中间检查。 因此,如果产品的功能不起作用,则逻辑上抛出AssumptionError异常,否则抛出RuntimeException。





在Allure中,将立即看到我们正在处理什么:要么我们存在产品缺陷(FT专家接替了工作),要么自动测试发生了故障(AT专家理解)。

扩展模型



用户有机会重新定义工具的基本功能或实现自己的功能。 Atlas扩展模型类似于JUnit 5扩展模型,atlas-webdriver和atlas-appium模块是基于扩展构建的。 如果您有兴趣,请查看源代码。

让我们研究一个抽象的示例:需要为Internet Explorer 11开发UI测试(在某些地方仍在使用)。 有时,标准单击元素不起作用,那么您可以使用JS单击。 您决定暂时替代对整个测试项目的单击。

 onMainPage().header().button("en").click(); 

怎么做?

创建一个实现MethodExtension接口的扩展。

 public class JSClickExt implements MethodExtension { @Override public Object invoke(Object proxy, MethodInfo methodInfo, Configuration config) { final WebDriver driver = config.getContext(WebDriverContext.class) .orElseThrow(() -> new AtlasException("Context doesn't exist")).getValue(); final JavascriptExecutor js = (JavascriptExecutor) driver; js.executeScript("arguments[0].click();", proxy); return proxy; } @Override public boolean test(Method method) { return method.getName().equals("click"); } } 

我们重新定义了两种方法。 在test()方法中,我们指定我们覆盖click方法。 invoke方法实现所需的逻辑。 现在,单击元素将通过JavaScript进行。

我们按以下方式连接扩展:

 atlas = new Atlas(new WebDriverConfiguration(driver, "https://github.com")) .extension(new JSClickExt()); 

借助扩展,可以搜索数据库中元素的定位符并实现其他有趣的功能-这一切都取决于您的想象力和需求。

PageObjects(WebSite)的单个入口点

该工具使您可以将所有页面存储在一个地方,并且将来只能通过Site实体进行工作。

 public interface GitHubSite extends WebSite { @Page MainPage onMainPage(); @Page(url = "search") SearchPage onSearchPage(@Query("q") String value); @Page(url = "{profile}/{project}/tree/master/") ProjectPage onProjectPage(@Path("profile") String profile, @Path("project") String project); @Page ContributorsPage onContributorsPage(); } 

此外,Page可以设置快速url,查询参数和路径段。

 onSite().onProjectPage("qameta", "atlas").contributors().click(); 

上面的行包含两个路径段(qameta和atlas),它们转换为github.com/qameta/atlas/tree/master 。 这种方法的主要优点是可以立即打开所需页面,而无需单击它。

 @Test public void usePathWebSiteTest() { onSite().onProjectPage("qameta", "atlas").contributors().click(); onSite().onContributorsPage().hovercards().waitUntil(hasSize(4)); } 

使用移动元素

使用移动元素(AtlasMobileElement)类似于使用AtlasWebElement Web元素。 此外,向AtlasMobileElement添加了三种方法:向上/向下滚动屏幕(scrollUp / scrollDown)并单击具有保持状态的项目(longPress)。

让我给您一个Wikipedia应用程序主屏幕的示例。 针对iOS平台和Android都描述了一个元素。 它们还描述了参数化按钮。

 public interface MainScreen extends Screen { @Retry(timeout = 20_000L, polling = 2000L) @IOSFindBy(xpath = "//XCUIElementTypeSearchField[@name='Search Wikipedia']") @AndroidFindBy(xpath = "//*[contains(@text, 'Search Wikipedia')]") AtlasMobileElement searchWikipedia(); @IOSFindBy(id = "{{ value }}") AtlasMobileElement button(@Param("value") String value); } 

测试看起来是一样的:

 public void simpleExample() { onMainScreen().searchWikipedia().click(); onSearchScreen().search().sendKeys("Atlas"); onSearchScreen().item("Atlas LV-3B").swipeDownOn().click(); onArticleScreen().articleTitle().should(allOf(displayed(), text("Atlas LV-3B"))); } 

在上面的示例中,我们打开Wikipedia主页,单击搜索栏,输入Atlas文本,然后滚动到值为Atlas LV-3B的列表项并进入其视图。 最后一行检查标题是否显示并包含所需的值。

听众

可以使用特殊的侦听器(侦听器接口)实现事件日志记录。 每个方法在调用时都有四个事件: BeforePassFail之后



使用此界面,您可以组织报告。 以下是“魅力监听器”的示例,可以在链接上找到。



接下来,在初始化Atlas类时连接侦听器。

 atlas = new Atlas(new WebDriverConfiguration(driver)).listener(new AllureListener()); 


以上述方式,可以为各种报告系统(例如,ReportPortal)创建一个侦听器。

连接方式



要使UI Web自动化,只需注册atlas-webdriver依赖项并指出最新的版本即可(在撰写本文时,相关的版本为1.6.0)。

Maven:
  <dependency> <groupId>io.qameta.atlas</groupId> <artifactId>atlas-webdriver</artifactId> <version>${atlas.version}</version> </dependency> 


摇篮:
 dependencies { ompile 'io.qameta.atlas:atlas-webdriver:1.+' } 


如果您要使UI Mobile自动化,我们会做同样的事情。

Maven:
 <dependency> <groupId>io.qameta.atlas</groupId> <artifactId>atlas-appium</artifactId> <version>${atlas.version}</version> </dependency> 


摇篮:
 dependencies { ompile 'io.qameta.atlas:atlas-appium:1.+' } 


使用方法



将依赖项添加到项目后,必须初始化Atlas类实例。

 @Before public void startDriver() { driver = new ChromeDriver(); atlas = new Atlas(new WebDriverConfiguration(driver)); } 

我们将配置实例以及驱动程序传递给Atlas构造函数。

当前有两种配置:WebDriverConfiguration和AppiumDriverConfiguration。 每个配置都包含特定的默认扩展名。

接下来,我们定义一个将创建所有PageObject的方法。

 private <T extends WebPage> T onPage(Class<T> page) { return atlas.create(driver, page); } 


一个简单的测试用例的例子:

 @Test public void simpleTest() { onPage(MainPage.class).open("https://github.com"); onPage(MainPage.class).header().searchInput().sendKeys("Atlas"); onPage(MainPage.class).header().searchInput().submit(); } 

我们打开站点,转到标题层,在其中查找文本字段(搜索输入),输入文本并按Enter。

总结


最后,我想指出的是,Atlas是一款功能强大的灵活工具。 可以为您的团队和您方便的方式针对特定的测试项目对其进行自定义。 开发跨平台测试等

提供了HeisenbugSelenium CampNexign QA Meetup 报告视频片段 。 有一个电报聊天室@atlashelp。

使用此工具,您可以减少大量的代码行(由Yandex,SberTech和Tinkoff等公司在项目上进行过测试)。

Source: https://habr.com/ru/post/zh-CN474408/


All Articles