selenium架构

在此博客文章中,我想介绍一种具有最佳设计模式的Selenium测试的简洁架构:页面对象,页面元素(通常称为HTML包装器)以及自行开发的非常小巧的框架。 该体系结构不限于示例中使用的Java,也可以以任何其他语言应用于Selenium测试。

定义和关系。

页面对象 。 页面对象封装了网页的行为。 每个网页有一个页面对象,将页面的逻辑抽象到外部。 这意味着,与网页的交互被封装在page对象中。 Selenium的“ 通过”定位器来查找页面上的元素也没有对外公开。 页面对象的调用者不应忙于By定位符,例如By.idBy.tageNameBy.cssSelector。Selenium测试类对页面对象进行操作。 从网上商店举个例子:页面对象类可以称为例如ProductPageShoppingCartPagePaymentPage等。这些始终是带有自己的URL的整个网页的类。

页面元素 (又名HTML包装器 )。 页面元素是网页的另一个细分。 它表示一个HTML元素,并封装了与此元素进行交互的逻辑。 我将页面元素称为HTML包装器。 HTML包装器是可重用的,因为多个页面可以包含相同的元素。 例如,用于DatepickerHTML包装器可以提供以下方法(API):“在输入字段中设置日期”,“打开日历弹出窗口”,“在日历弹出窗口中选择给定的日期”等。其他HTML包装可以例如,自动完成,面包屑,复选框,单选按钮,MultiSelect,消息等。HTML包装器可以是复合的。 这意味着它可以包含多个小元素。 例如,产品目录由产品组成,购物车由项目组成,等等。内部元素的Selenium的“ 按”定位符封装在复合页面元素中。

Martin Fowler描述了页面对象和HTML包装器作为设计模式。

Selenium测试类的骨骼结构。

测试类的结构良好。 它以单个过程步骤的形式定义测试顺序。 我建议采用以下结构:

public class MyTestIT extends AbstractSeleniumTest {@FlowOnPage(step = 1, desc = "Description for this method")void flowSomePage(SomePage somePage) {...}@FlowOnPage(step = 2, desc = "Description for this method")void flowAnotherPage(AnotherPage anotherPage) {...}@FlowOnPage(step = 3, desc = "Description for this method")void flowYetAnotherPage(YetAnotherPage yetAnotherPage) {...}...
}

MyTestIT类是用于集成测试的JUnit测试类。 @FlowOnPage是网页上测试逻辑的方法注释。 step参数定义测试序列中的序列号。 计数从1开始。这意味着,将在步骤= 2的方法之前处理步骤= 1的带注释的方法。第二个参数desc表示描述该方法的作用。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FlowOnPage {int step() default 1;String desc();
}

带页面对象作为方法参数调用带注释的方法。 通常,每次单击按钮或链接都会切换到下一页。 开发的框架应确保在调用带有下一步的带注释的方法之前,已完全加载下一页。 下图说明了测试类,页面对象和HTML包装器之间的关系。

别这样 用@Test注释的JUnit方法在哪里,解析@FlowOnPage注释的逻辑在哪里 ? 该代码隐藏在超类AbstractSeleniumTest中

public abstract class AbstractSeleniumTest {// configurable base URLprivate final String baseUrl = System.getProperty("selenium.baseUrl", "http://localhost:8080/contextRoot/");private final WebDriver driver;public AbstractSeleniumTest() {// create desired WebDriverdriver = new ChromeDriver();// you can also set here desired capabilities and so on...}/*** The single entry point to prepare and run test flow.*/@Testpublic void testIt() throws Exception {LoadablePage lastPageInFlow = null;List <Method> methods = new ArrayList<>();// Seach methods annotated with FlowOnPage in this and all super classesClass c = this.getClass();while (c != null) {for (Method method: c.getDeclaredMethods()) {if (method.isAnnotationPresent(FlowOnPage.class)) {FlowOnPage flowOnPage = method.getAnnotation(FlowOnPage.class);// add the method at the right positionmethods.add(flowOnPage.step() - 1, method);}}c = c.getSuperclass();}for (Method m: methods) {Class<?>[] pTypes = m.getParameterTypes();LoadablePage loadablePage = null;if (pTypes != null && pTypes.length > 0) {loadablePage = (LoadablePage) pTypes[0].newInstance();}if (loadablePage == null) {throw new IllegalArgumentException("No Page Object as parameter has been found for the method " +m.getName() + ", in the class " + this.getClass().getName());}// initialize Page Objects Page-Objekte and set parent-child relationshiploadablePage.init(this, m, lastPageInFlow);lastPageInFlow = loadablePage;}if (lastPageInFlow == null) {throw new IllegalStateException("Page Object to start the test was not found");}// start testlastPageInFlow.get();}/*** Executes the test flow logic on a given page.** @throws AssertionError can be thrown by JUnit assertions*/public void executeFlowOnPage(LoadablePage page) {Method m = page.getMethod();if (m != null) {// execute the method annotated with FlowOnPagetry {m.setAccessible(true);m.invoke(this, page);} catch (Exception e) {throw new AssertionError("Method invocation " + m.getName() +", in the class " + page.getClass().getName() + ", failed", e);}}}@Afterpublic void tearDown() {// close browserdriver.quit();}/*** This method is invoked by LoadablePage.*/public String getUrlToGo(String path) {return baseUrl + path;}public WebDriver getDriver() {return driver;}
}

如您所见,只有一种测试方法testIt可以解析批注,创建具有关系的页面对象并启动测试流程。

页面对象的结构。

每个页面对象类均从LoadablePage类继承,而该类又从Selenium的LoadableComponent类继承。 这篇写得很好的文章对L oadableComponent进行了很好的解释: LoadableComponent的简单和高级用法 。 LoadablePage是我们自己的类,实现如下:

import org.openqa.selenium.support.ui.WebDriverWait;
import org.junit.Assert;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.LoadableComponent;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.List;public abstract class LoadablePage<T extends LoadableComponent<T>> extends LoadableComponent<T> {private final static Logger LOGGER = LoggerFactory.getLogger(LoadablePage.class);private AbstractSeleniumTest seleniumTest;private String pageUrl;private Method method;private LoadablePage parent;/*** Init method (invoked by the framework).** @param seleniumTest instance of type AbstractSeleniumTest* @param method to be invoked method annotated with @FlowOnPage* @param parent parent page of type LoadablePage*/void init(AbstractSeleniumTest seleniumTest, Method method, LoadablePage parent) {this.seleniumTest = seleniumTest;this.pageUrl = seleniumTest.getUrlToGo(getUrlPath());this.method = method;this.parent = parent;PageFactory.initElements(getDriver(), this);}/*** Path of the URL without the context root for this page.** @return String path of the URL*/protected abstract String getUrlPath();/**** Specific check which has to be implemented by every page object.* A rudimentary check on the basis of URL is undertaken by this class.* This method is doing an extra check if the page has been proper loaded.** @throws Error thrown when the check fails*/protected abstract void isPageLoaded() throws Error;@Overrideprotected void isLoaded() throws Error {// min. check against the page URLString url = getDriver().getCurrentUrl();Assert.assertTrue("You are not on the right page.", url.equals(pageUrl));// call specific check which has to be implemented on every pageisPageLoaded();}@Overrideprotected void load() {if (parent != null) {// call the logic in the parent pageparent.get();// parent page has navigated to this page (via click on button or link).// wait until this page has been loaded.WebDriverWait wait = new WebDriverWait(getDriver(), 20, 250);wait.until(new ExpectedCondition<Boolean> () {@Overridepublic Boolean apply(WebDriver d) {try {isLoaded();return true;} catch (AssertionError e) {return false;}}});} else {// Is there no parent page, the page should be navigated directlyLOGGER.info("Browser: {}, GET {}", getDriver(), getPageUrl());getDriver().get(getPageUrl());}}/*** Ensure that this page has been loaded and execute the test code on the this page.** @return T LoadablePage*/public T get() {T loadablePage = super.get();// execute flow logicseleniumTest.executeFlowOnPage(this);return loadablePage;}/*** See {@link WebDriver#findElement(By)}*/public WebElement findElement(By by) {return getDriver().findElement(by);}/*** See {@link WebDriver#findElements(By)}*/public List<WebElement> findElements(By by) {return getDriver().findElements(by);}public WebDriver getDriver() {return seleniumTest.getDriver();}protected String getPageUrl() {return pageUrl;}Method getMethod() {return method;}
}

如您所见,每个页面对象类都需要实现两个抽象方法:

/*** Path of the URL without the context root for this page.** @return String path of the URL*/
protected abstract String getUrlPath();/**** Specific check which has to be implemented by every page object.* A rudimentary check on the basis of URL is undertaken by the super class.* This method is doing an extra check if the page has been proper loaded.** @throws Error thrown when the check fails*/
protected abstract void isPageLoaded() throws Error;

现在,我想显示一个具体页面对象的代码和一个测试SBB Ticket Shop的测试类,以便读者可以对页面对象进行测试。 页面对象TimetablePage包含基本元素HTML包装器。

public class TimetablePage extends LoadablePage<TimetablePage> {@FindBy(id = "...")private Autocomplete from;@FindBy(id = "...")private Autocomplete to;@FindBy(id = "...")private Datepicker date;@FindBy(id = "...")private TimeInput time;@FindBy(id = "...")private Button search;@Overrideprotected String getUrlPath() {return "pages/fahrplan/fahrplan.xhtml";}@Overrideprotected void isPageLoaded() throws Error {try {assertTrue(findElement(By.id("shopForm_searchfields")).isDisplayed());} catch (NoSuchElementException ex) {throw new AssertionError();}}public TimetablePage typeFrom(String text) {from.setValue(text);return this;}public TimetablePage typeTo(String text) {to.setValue(text);return this;}public TimetablePage typeTime(Date date) {time.setValue(date);return this;}public TimetablePage typeDate(Date date) {date.setValue(date);return this;}public TimetablePage search() {search.clickAndWaitUntil().ajaxCompleted().elementVisible(By.cssSelector("..."));return this;}public TimetableTable getTimetableTable() {List<WebElement> element = findElements(By.id("..."));if (element.size() == 1) {return TimetableTable.create(element.get(0));}return null;}
}

在页面对象中,可以通过@FindBy@FindBys@FindAll批注或按需动态创建HTML包装器(简单或复合),例如,如TimetableTable.create(element) ,其中element是基础WebElement 。 通常,注释不适用于自定义元素。 默认情况下,它们仅与Selenium的WebElement一起使用。 但是让他们也使用自定义元素并不难。 您必须实现一个扩展DefaultFieldDecorator的自定义FieldDecorator 。 自定义FieldDecorator允许对自定义HTML包装程序使用@FindBy@FindBys@FindAll批注。 此处提供了一个示例项目,其中提供了实现细节和自定义元素的示例。 您还可以赶上Selenium的臭名昭著的StaleElementReferenceException在您的自定义FieldDecorator并重新创建由最初定位的基础WebElement。 框架用户看不到StaleElementReferenceException,并且即使在此期间更新了引用的DOM元素(从DOM删除并再次添加新内容)时,也可以在WebElement上调用方法。 此处提供了带有代码段的想法。

好吧,让我展示测试课程。 在测试班中,我们要测试当16岁以下的儿童没有父母旅行时购物车中是否出现提示。 首先,我们必须输入“从”和“到”的车站,在时间表中单击所需的连接,然后在下一页添加一个子项,该子项显示所选连接的旅行报价。

public class HintTravelerIT extends AbstractSeleniumTest {@FlowOnPage(step = 1, desc = "Seach a connection from Bern to Zürich and click on the first 'Buy' button")void flowTimetable(TimetablePage timetablePage) {// Type from, to, date and timetimetablePage.typeFrom("Bern").typeTo("Zürich");Date date = DateUtils.addDays(new Date(), 2);timetablePage.typeDate(date);timetablePage.typeTime(date);// search for connectionstimetablePage.search();// click on the first 'Buy' buttonTimetableTable table = timetablePage.getTimetableTable();table.clickFirstBuyButton();}@FlowOnPage(step = 2, desc = "Add a child as traveler and test the hint in the shopping cart")void flowOffers(OffersPage offersPage) {// Add a childDateFormat df = new SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN);String birthDay = df.format(DateUtils.addYears(new Date(), -10));offersPage.addTraveler(0, "Max", "Mustermann", birthDay);offersPage.saveTraveler();// Get hintsList<String> hints = offersPage.getShoppingCart().getHints();assertNotNull(hints);assertTrue(hints.size() == 1);assertEquals("A child can only travel under adult supervision", hints.get(0));}
}

HTML包装器的结构。

我建议为所有HTML包装器创建一个抽象基类。 我们称之为HtmlWrapper 。 此类可以提供一些常用方法,例如clickclickAndWaitUntilfindElement(s)getParentElementgetAttributeisDisplayed ……对于可编辑元素,您可以创建一个继承自HtmlWrapper的类EditableWrapper 。 此类可以为可编辑元素提供一些常用方法,例如: clear (清除输入), enter (按下Enter键), isEnabled (检查元素是否已启用),…。所有可编辑元素都应继承自EditableWrapper 。 此外,您可以分别为单值和多值元素提供两个接口EditableSingleValueEditableMultipleValue 。 下图展示了这个想法。 它显示了三个基本HTML包装的类层次结构:

  • 日期选择器 。 它继承自EditableWrapper并实现EditableSingleValue接口。
  • MultiSelect 。 它继承自EditableWrapper并实现EditableMultiValue接口。
  • 留言 。 它直接扩展HtmlWrapper,因为消息不可编辑。

您是否需要更多有关HTML包装程序的实现细节? jQuery Datepicker的详细信息可以在这篇出色的文章中找到 。 MultiSelect是著名的Select2小部件的包装。 我已经通过以下方式在项目中实现了包装器:

public class MultiSelect extends EditableWrapper implements EditableMultiValue<String> {protected MultiSelect(WebElement element) {super(element);}public static MultiSelect create(WebElement element) {assertNotNull(element);return new MultiSelect(element);}@Overridepublic void clear() {JavascriptExecutor js = (JavascriptExecutor) getDriver();js.executeScript("jQuery(arguments[0]).val(null).trigger('change')", element);}public void removeValue(String...value) {if (value == null || value.length == 0) {return;}JavascriptExecutor js = (JavascriptExecutor) getDriver();Object selectedValues = js.executeScript("return jQuery(arguments[0]).val()", element);String[] curValue = convertValues(selectedValues);String[] newValue = ArrayUtils.removeElements(curValue, value);if (newValue == null || newValue.length == 0) {clear();} else {changeValue(newValue);}}public void addValue(String...value) {if (value == null || value.length == 0) {return;}JavascriptExecutor js = (JavascriptExecutor) getDriver();Object selectedValues = js.executeScript("return jQuery(arguments[0]).val()", element);String[] curValue = convertValues(selectedValues);String[] newValue = ArrayUtils.addAll(curValue, value);changeValue(newValue);}@Overridepublic void setValue(String...value) {clear();if (value == null || value.length == 0) {return;}changeValue(value);}@Overridepublic String[] getValue() {JavascriptExecutor js = (JavascriptExecutor) getDriver();Object values = js.executeScript("return jQuery(arguments[0]).val()", element);return convertValues(values);}private void changeValue(String...value) {Gson gson = new Gson();String jsonArray = gson.toJson(value);String jsCode = String.format("jQuery(arguments[0]).val(%s).trigger('change')", jsonArray);JavascriptExecutor js = (JavascriptExecutor) getDriver();js.executeScript(jsCode, element);}@SuppressWarnings("unchecked")private String[] convertValues(Object values) {if (values == null) {return null;}if (values.getClass().isArray()) {return (String[]) values;} else if (values instanceof List) {List<String> list = (List<String> ) values;return list.toArray(new String[list.size()]);} else {throw new WebDriverException("Unsupported value for MultiSelect: " + values.getClass());}}
}

为了完整起见,还有一个Message实现的示例:

public class Message extends HtmlWrapper {public enum Severity {INFO("info"),WARNING("warn"),ERROR("error");Severity(String severity) {this.severity = severity;}private final String severity;public String getSeverity() {return severity;}}protected Message(WebElement element) {super(element);}public static Message create(WebElement element) {assertNotNull(element);return new Message(element);}public boolean isAnyMessageExist(Severity severity) {List<WebElement> messages = findElements(By.cssSelector(".ui-messages .ui-messages-" + severity.getSeverity()));return messages.size() > 0;}public boolean isAnyMessageExist() {for (Severity severity: Severity.values()) {List<WebElement> messages = findElements(By.cssSelector(".ui-messages .ui-messages-" + severity.getSeverity()));if (messages.size() > 0) {return true;}}return false;}public List<String> getMessages(Severity severity) {List<WebElement> messages = findElements(By.cssSelector(".ui-messages .ui-messages-" + severity.getSeverity() + "-summary"));if (messages.isEmpty()) {return null;}List<String> text = new ArrayList<> ();for (WebElement element: messages) {text.add(element.getText());}return text;}
}

消息将消息组件包装在PrimeFaces中 。

结论

完成页面对象和HTML包装程序的编写后,您可以安心并专注于舒适地编写Selenium测试。 随时分享您的想法。

翻译自: https://www.javacodegeeks.com/2016/04/clean-architecture-selenium-tests.html

selenium架构

selenium架构_Selenium测试的干净架构相关推荐

  1. selenium优缺点_Selenium测试自动化工具的优缺点

    selenium优缺点 谁不喜欢深背景的命名故事? 在2000年代初期,一家名为Mercury Interactive的公司通过其工具Astra Quick Test统治了测试自动化领域. 这不是可以 ...

  2. selenium 示例_Selenium测试中所需的功能和示例

    selenium 示例 所需功能是用于声明一组基本要求(例如浏览器,操作系统,浏览器版本等的组合)的类,以对Web应用程序执行自动跨浏览器测试. 当我们尝试通过Selenium自动化测试来自动化测试脚 ...

  3. selenium自动化测试_Selenium测试可实现有效的测试自动化

    selenium自动化测试 尽管移动应用程序的市场份额有大幅增长,但网络应用程序仍然具有大量的用户基础. 在部署之前,企业正在关注Web应用程序的质量. 这是测试发挥重要作用的地方. 与某些可以自动化 ...

  4. 干净架构在 Web 服务开发中的实践

    干净架构(The Clean Architecture)是 Bob 大叔在 2012 年的一篇博文 The Clean Architecture 中,提出的一种适用于复杂业务系统的软件架构方式.干净架 ...

  5. [四川金信石招人]技术总监架构师测试工程师

    四川金信石信息技术有限公司(Sichuan Kingscheme Information Technology Co.Ltd.,缩写为KSI)近200人专业精英团队,全国有5家子公司,专注移动互联网领 ...

  6. 瞎谈干净架构(clean architecture)

    写在前面的话:最近正在写一个自己的开源项目,打算使用像rxjava,retrofit,dagger2等开源框架,结果一个多月过去了,大框架基本写好了,却因为布局想绚丽一点,一直都没确定,最后拖延症犯了 ...

  7. [原创] 浅谈ETL系统架构如何测试?

    [原创] 浅谈ETL系统架构如何测试? 来新公司已入职3个月时间,由于公司所处于互联网基金行业,基金天然固有特点,基金业务复杂,基金数据信息众多,基金经理众多等,所以大家可想一下,基民要想赚钱真不容易 ...

  8. ORAN专题系列-29:运营商O-RAN扩展皮站测试的硬件架构

    作者主页:文火冰糖(王文兵)的博客_文火冰糖的硅基工坊_CSDN博客 本文网址:原创 ORAN专题系列-29:运营商O-RAN扩展皮站测试的硬件架构_文火冰糖(王文兵)的博客-CSDN博客 目录 第1 ...

  9. pcmark2 android测试,PCMark安卓版升级:支持64位ARM架构 存储测试为Storage 2.0

    作为一款跨平台的评测软件,PCMark的Android版本最近升级到了v3.0.4054.这个版本变化很大,官方支持64位ARM架构,跑分不能向下比较. ARM处理器长期支持64位,但支持的APP应用 ...

最新文章

  1. 网站的 计算机主机作用是什么意思,网关是什么意思?网关的作用是什么?
  2. Kubernetes入门
  3. QIIME 2教程. 27语义类型Semantic(2021.2)
  4. 劳动节特别活动,钉钉、支付宝合种,2-4天领证,限量9个名额
  5. 【2013年总结】 向着IT前进
  6. html多行文本框_前端面试集锦 HTML篇
  7. 【LeetCode算法题库】Day5:Roman to Integer Longest Common Prefix 3Sum
  8. 最短路的几种算法及其优化(模板)
  9. 解决AttributeError: 'str' object has no attribute 'decode'报错问题
  10. 动态规划 POJ 1088 滑雪
  11. oracle 建分区索引_ORACLE 创建组合分区索引
  12. 编程开发之--单例模式(2)
  13. android 按钮顶级效果_Android 中按钮的点击效果
  14. HDU1716 排列2【全排列+输出格式】
  15. 微信小程序开发工具下载安装
  16. 为何公众号推送会延迟发送_微信公众号客服消息群发和推送功能如何实现?
  17. php gd ttf,linux gd php TTF字体
  18. 从SAP顾问教JAVA开发说起
  19. python 白噪声检验-利用python实现平稳时间序列的建模方式
  20. 计算机语言不能用中文吗,为什么不能用中文来做编程呢?

热门文章

  1. P4092-[HEOI2016/TJOI2016]树【线段树,倍增】
  2. jzoj6316-djq的朋友圈【状压dp】
  3. 信息学奥赛一本通1349-最优布线问题
  4. 【c++算法刷题笔记】——洛谷1
  5. 【excrt】屠龙勇士(luogu 4774)
  6. 【dfs】益智游戏(2017 特长生 T2)
  7. 【期望】彩色圆环(金牌导航 期望-5)
  8. IntelliJ IDEA 源值1.5已过时,将在未来所有版本中删除
  9. Java通过Class的对象来获取泛型的class示例
  10. 14-多对多关系建表