Saturday, May 31, 2014

decorating selenium

On Selenium HQ home page, selenium is defined as following,

Selenium automates browsers. That's it! What you do with that power is entirely up to you. Primarily, it is for automating web applications for testing purposes, but is certainly not limited to just that. Boring web-based administration tasks can (and should!) also be automated as well. Selenium has the support of some of the largest browser vendors who have taken (or are taking) steps to make Selenium a native part of their browser. It is also the core technology in countless other browser automation tools, APIs and frameworks.

Even since Selenium came into existence, it has been adopted by many projects as test automation library. Due to its open source nature, it became a preferred test tool in many organizations.

Selenium is not without shortcomings, it is a library, not a framework, so it doesn't provide mechanisms to reduce duplication. To write clean tests, it is necessary to add a level of abstraction on top of Selenium to form an enterprise framework. Selenium Capsules, is such an enterprise framework allowing developers to maximize theirs productivity by offering services to relieve developers from worrying about many common concerns such waiting, exception handling and logging. It also provides reference implementation to teach developers how to organize software artifacts. Behind the scene, this Selenium Capsules framework, is just an application of Decorator Pattern.



Let us have a look of the WebDriver interface,



WebDriver is an interface, it has many implementation classes such as FirefoxDriver, ChromeDriver, etc. When we use its method findElement(By), we may run into situation when the element is loaded by an Ajax request and we need to give instruction to the test to wait for a while until the element is displayed before calling it, otherwise, it would throw a NoSuchElementException. All these bullet plate codes will make the test code verbose and more difficult to find useful information. Thus it is desirable to attach additional logic to save developers from repeating this condition check and exception handling. However, since there are so many browsers implementation classes, it is not practical to extend all of them to add this waiting mechanism. Even if it is possible to extend those classes to add this override method, this method will be repeated in all of those concrete driver classes.



Decorator Pattern, a 20 years old design pattern by 2014, effectively solved our problem.

Another great tool is the default method in Java 8. In Java 8, some new methods are added to existing interface. As Java developers, we all know if a method is added to an interface, all implementing class need to be modified to add that new method, otherwise, it is a compilation error. That's why JDK interfaces have never changed. It is not the case any more. In Java 8, a new language feature is added to make it is possible to add new method into an existing interface, it is default method.

Default methods are methods with implementations in the interface. You may wonder, will this change make an interface an abstract class? No, Interface is still not an abstract class, even interface can have default methods, but it can't have instance variables, so the default method can only call method of a local variable, or methods from the same interface, thus interfaces still can't replace abstract classes since abstract classes can have instance variables.

An interface Browser, which extends WebDriver, is introduced into the framework to decorate WebDriver with additional responsibilities and it also provide default implementation of all the methods from WebDriver by just delegating the calls to WebDriver.



What's the reason to spend so much effort to decorate WebDriver? Many people may have this doubt at the beginning. The reason is simple, to attach additional responsibility to address some common concerns. This method in Browser interface takes advantage of some Java 8 syntax and transforms all WebElements it finds into Element classes, also its parameter is Supplier, not a By class like the original findElements method in the WebDriver, this feature is called method overloading in Object Oriented languages,
    default public Stream<Element> findElements(Supplier<By> by) {
        return findElements(by.get()).stream().map(Element::new);
    }


Stream, Supplier and Element::new are Java 8 features which is covered in this blog, functional selenium.

Now that we decorated WebDriver to return Element object which is a decorator of WebElement, let us compare it the raw WebElement object to see what's the advantages it gives. Here are the methods of WebElement,



We need to do the same thing as the decorator to WebDriver, to add new methods to the extended interface to have the following method,

    default public Stream<Element> findElements(Supplier<By> by) {
        return findElements(by.get()).stream().map(Element::new);
    }


Ultimately, we decorated SearchContext and added many new search methods to locate popular web widgets,by the assistance from default methods from Java 8, all those new methods are available to the implementation classes without extra effort for implementing them.

public interface Searchable<Where extends Searchable<Where>> extends SearchContext, Waitable<Where> {

    /**
     * Find the first element or throw NoSuchElementException
     *
     * @param by selector
     * @return the first element or throw NoSuchElementException
     */
    @Override
    Element findElement(By by);

    /**
     * Find the first element or return null if nothing found.
     *
     * @param by selector
     * @return the first element or return null if nothing found.
     */
    default public Element tryElement(By by) {
        try {
            return new Element(findElement(by));
        } catch (NoSuchElementException e) {
            return null;
        }
    }

    /**
     * Find the first element or throw NoSuchElementException
     *
     * @param by selector
     * @return the first element or throw NoSuchElementException
     */
    default public Element untilFound(final By by) {
        return until((Where page) -> new Element(findElement(by)));
    }

    /**
     * Find all elements within the area using the given search method.
     *
     * @param by selector
     * @return A stream of all {@link Element}s, or an empty stream if nothing matches.
     * @see org.openqa.selenium.By
     */
    default public Stream<Element> findElements(Supplier<By> by) {
        return findElements(by.get()).stream().map(Element::new);
    }

    /**
     * Find the first button meeting the By method.
     * method to find the button.
     *
     * @param by selector
     * @return
     */
    default public Clickable button(Supplier<By> by) {
        return button(by, 0);
    }

    /**
     * If there are multiple buttons with the same name on the same page, use this
     * method to find the button.
     *
     * @param by    selector
     * @param index
     * @return
     */
    @SuppressWarnings("unchecked")
    default public Clickable button(Supplier<By> by, int index) {
        return new Button<>((Where) this, Locators.<Where>elements(by)
                .and(new StreamToList<>())
                .and(new ElementAtIndex<>(index)));
    }

    /**
     * If the button can't be found using the previous two methods, use this.
     *
     * @param locator
     * @return
     */
    @SuppressWarnings("unchecked")
    default public Clickable button(Locator<Where, Element> locator) {
        return new Button<>((Where) this, locator);
    }

    /**
     * The first image using the image file.
     *
     * @param fileName
     * @return
     */
    default public Element image(String fileName) {
        return new FirstItem<Element>().locate(images(fileName));
    }

    /**
     * The image at the given index using the same image file.
     *
     * @param fileName
     * @param index
     * @return
     */
    default public Element image(String fileName, int index) {
        return new StreamToList<Element>()
                .and(new ElementAtIndex<>(index))
                .locate(images(fileName));
    }

    /**
     * Find the images using the same image file.
     *
     * @param fileName
     * @return the images  using the same image file.
     */
    default public Stream<Element> images(String fileName) {
        return until(Locators.<Where>elements(IMG)
                        .and(new Filter<>(DISPLAYED.and(SRC.and(new StringContains(fileName)))))
        );
    }

    /**
     * Find the link using the selector.
     *
     * @param selector
     * @return
     */
    @SuppressWarnings("unchecked")
    default public Clickable link(Supplier<By> selector) {
        return new Link<>((Where) this, element(selector));
    }
}



The new method tryElement will return null if it can't find the element immediately, and untilFound method will wait until timeout so either returns the element it is looking for or throw an NoSuchElementException. These new methods make it easier for testing AJAX enabled web applications which requires the extensive usages of try and wait.

    /**
     * Find the first element or return null if nothing found.
     *
     * @param by selector
     * @return the first element or return null if nothing found.
     */
    default public Element tryElement(By by) {
        try {
            return new Element(findElement(by));
        } catch (NoSuchElementException e) {
            return null;
        }
    }

    /**
     * Find the first element or throw NoSuchElementException
     *
     * @param by selector
     * @return the first element or throw NoSuchElementException
     */
    default public Element untilFound(final By by) {
        return until((Where page) -> new Element(findElement(by)));
    }


The untilFound method effectively added support for Explicit Waits without cluttering your tests with millions lines of code like this,

   // ☹ this is a bad example, please don't follow the style.
   FluentWait<By> fluentWait = new FluentWait<By>(By.tagName("TEXTAREA"));  \\ define element for which you want to poll
   fluentWait.pollingEvery(300, TimeUnit.MILLISECONDS); \\ it will ping for every 3 sec
   fluentWait.withTimeout(1000, TimeUnit.MILLISECONDS);  \\ max time out
   fluentWait.until(new Predicate<By>() {
      public boolean apply(By by) {
         try {
           return browser.findElement(by).isDisplayed();
         } catch (NoSuchElementException ex) {
           return false;
         }
      }
   });
   browser.findElement(By.tagName("TEXTAREA")).sendKeys("text to enter");
You can use call this method to enter data into a text input field on form,

   page.put(() -> By.tagName("TEXTAREA"), "text to enter");
Element is an implementation class of both WebElement and Searchable interfaces, thus, it can be used as source and target elements of a drag and drop action.

    @Test
    public void testDragAndDrop() {
        Element source = CHROME.findElement(By.name("source"));
        Element target = CHROME.findElement(By.name("target"));
        Actions actions = new Actions(CHROME);
        actions.dragAndDrop(source, target);
    }
It is cleaner if you use this method, it has the same effect as the one above.

    @Test
    public void testDragAndDrop() {
        CHROME.dragAndDrop(id("drag1"), id("div2"));
    }


As a comparison, here is the code without using framework,

    //This is an ugly test not using page framework, it has the same function as the test above. :(
    @Test
    public void dragAndDropChrome() throws InterruptedException {
        System.setProperty("webdriver.chrome.driver", "src/main/resources/chrome/chromedriver");
        WebDriver webDriver = new ChromeDriver();
        webDriver.get("http://www.w3schools.com/html/html5_draganddrop.asp");
        WebElement source = webDriver.findElement(id("drag1"));
        System.out.println(source.getAttribute("src"));
        WebElement target = webDriver.findElement(id("div2"));
        System.out.println(target.getTagName() + "=" + target.toString());

        Actions builder = new Actions(webDriver);
        Action dragAndDrop = builder.clickAndHold(source)
                .moveToElement(target)
                .release(source)
                .build();
        dragAndDrop.perform();
    }



From these examples, it is clear that the tests written using Selenium Capsules framework are much cleaner than the tests written using Selenium directly. This is the reason to decorate Selenium and the sole purpose of Selenium Capsules, to encapsulate selenium powder into clean capsules and make it easy to serve.

No comments:

Post a Comment