selenium架构_Selenium测试的干净架构
selenium架構
在此博客文章中,我想介紹一種具有最佳設計模式的Selenium測試的簡潔架構:頁面對象,頁面元素(通常稱為HTML包裝器)以及自行開發的非常小巧的框架。 該體系結構不限于示例中使用的Java,也可以以任何其他語言應用于Selenium測試。
定義和關系。
頁面對象 。 頁面對象封裝了網頁的行為。 每個網頁有一個頁面對象,將頁面的邏輯抽象到外部。 這意味著,與網頁的交互被封裝在page對象中。 Selenium的“ 通過”定位器來查找頁面上的元素也沒有對外公開。 頁面對象的調用者不應忙于By定位符,例如By.id , By.tageName , By.cssSelector等。Selenium測試類對頁面對象進行操作。 從網上商店舉個例子:頁面對象類可以稱為例如ProductPage , ShoppingCartPage , PaymentPage等。這些始終是帶有自己的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 。 此類可以提供一些常用方法,例如click , clickAndWaitUntil , findElement(s) , getParentElement , getAttribute , isDisplayed ……對于可編輯元素,您可以創建一個繼承自HtmlWrapper的類EditableWrapper 。 此類可以為可編輯元素提供一些常用方法,例如: clear (清除輸入), enter (按下Enter鍵), isEnabled (檢查元素是否已啟用),…。所有可編輯元素都應繼承自EditableWrapper 。 此外,您可以分別為單值和多值元素提供兩個接口EditableSingleValue和EditableMultipleValue 。 下圖展示了這個想法。 它顯示了三個基本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测试的干净架构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电源指示灯不亮美的电热水器电源指示灯不亮
- 下一篇: 为了这波分区电脑桌面壁纸电脑如何分区壁纸