硒测试的干净架构

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

定义和关系。

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

页面元素 (又名HTML包装器 )。 页面元素是网页的另一个细分。 它表示一个HTML元素,并封装了与此元素进行交互的逻辑。 我将页面元素称为HTML包装器。 HTML包装器是可重用的,因为多个页面可以包含相同的元素。 例如,DatepickerHTML包装器可以提供以下方法(API):“在输入字段中设置日期”,“打开日历弹出窗口”,“在日历弹出窗口中选择给定的日期”等。其他HTML包装可以例如,“自动完成”,“面包屑”,“复选框”,“单选按钮”,“多选”,“消息”等。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 URL
    private final String baseUrl = System.getProperty("selenium.baseUrl", "http://localhost:8080/contextRoot/");

    private final WebDriver driver;

    public AbstractSeleniumTest() {
        // create desired WebDriver
        driver = new ChromeDriver();

        // you can also set here desired capabilities and so on
        ...
    }

    /**
     * The single entry point to prepare and run test flow.
     */
    @Test
    public void testIt() throws Exception {
        LoadablePage lastPageInFlow = null;
        List <Method> methods = new ArrayList<>();

        // Seach methods annotated with FlowOnPage in this and all super classes
        Class 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 position
                    methods.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 relationship
            loadablePage.init(this, m, lastPageInFlow);
            lastPageInFlow = loadablePage;
        }

        if (lastPageInFlow == null) {
            throw new IllegalStateException("Page Object to start the test was not found");
        }

        // start test
        lastPageInFlow.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 FlowOnPage
            try {
                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);
            }
        }
    }

    @After
    public void tearDown() {
        // close browser
        driver.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;

    @Override
    protected void isLoaded() throws Error {
        // min. check against the page URL
        String 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 page
        isPageLoaded();
    }

    @Override
    protected void load() {
        if (parent != null) {
            // call the logic in the parent page
            parent.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> () {
                @Override
                public Boolean apply(WebDriver d) {
                    try {
                        isLoaded();
                        return true;
                    } catch (AssertionError e) {
                        return false;
                    }
                }
            });
        } else {
            // Is there no parent page, the page should be navigated directly
            LOGGER.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 logic
        seleniumTest.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;

    @Override
    protected String getUrlPath() {
        return "pages/fahrplan/fahrplan.xhtml";
    }

    @Override
    protected 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批注。 这里提供一个示例项目,其中提供了实现细节和自定义元素的示例。 您还可以赶上硒的臭名昭著的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 time
        timetablePage.typeFrom("Bern").typeTo("Zürich");
        Date date = DateUtils.addDays(new Date(), 2);
        timetablePage.typeDate(date);
        timetablePage.typeTime(date);

        // search for connections
        timetablePage.search();

        // click on the first 'Buy' button
        TimetableTable 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 child
        DateFormat 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 hints
        List<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);
    }

    @Override
    public 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);
    }

    @Override
    public void setValue(String...value) {
        clear();
        if (value == null || value.length == 0) {
            return;
        }

        changeValue(value);
    }

    @Override
    public 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值