Thursday, July 24, 2014

to be continued ...

I signed a contract with Manning Publisher to prepare to publish a book called "Selenium in Practice", part of the book will come from this blog, Selenium Capsules. If you like this blog, probably you will like the book better since I will be getting help from professional editors to polish my work to the Manning Quality. I know this blog is far from Manning Quality, that's the reason I reached out to them. I believe the ideas in Selenium Capsules are marvelous and the design patterns used in the framework are not only elegant but can save you project time dramatically.

A book from Manning will let more people know about the framework and use it to write better, cleaner tests faster.

Better, cleaner, faster. That's the goal of Selenium Capsules.

Better, cleaner, faster. That's also the goal of Selenium In Practice.

Please feel free to send questions and suggestions by commenting this blog.

Should I call the book Selenium Capsules? I like this name better than Selenium in Practice but it is kind of risky.

Thank you.

Saturday, June 14, 2014

handle ajax

When testing web sites built with AJAX technology, special cares must be taken to make sure when you call webDriver.findElement. Here is a web site selling event tickets, Ticketfly, to test the change location function, you may be attempted to write a test like this,

    @Test
    public void changeLocationUsingSelenium() {
        System.setProperty("webdriver.chrome.driver", "src/main/resources/chrome/chromedriver");
        WebDriver webDriver = new ChromeDriver();
        webDriver.get("http://www.ticketfly.com");
        webDriver.findElement(linkText("change location")).click();
        webDriver.findElement(linkText("CANADA")).click();
        webDriver.findElement(linkText("All Canada")).click();
        assertEquals("Canada", 
                     webDriver.findElement(By.className("tools-location"))
                              .findElement(By.tagName("a"))
                              .findElement(By.tagName("strong"))
                              .getText());
    }


Unfortunately, this test doesn't work, you will get this exception when you try to run the test,

Starting ChromeDriver (v2.10.267517) on port 39666
Only local connections are allowed.
log4j:WARN No appenders could be found for logger (org.apache.http.client.protocol.RequestAddCookies).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

org.openqa.selenium.NoSuchElementException: no such element
  (Session info: chrome=35.0.1916.153)
  (Driver info: chromedriver=2.10.267517,platform=Mac OS X 10.9.3 x86_64) (WARNING: The server did not provide any stacktrace information)
Command duration or timeout: 115 milliseconds
For documentation on this error, please visit: http://seleniumhq.org/exceptions/no_such_element.html
Build info: version: '2.42.2', revision: '6a6995d31c7c56c340d6f45a76976d43506cd6cc', time: '2014-06-03 10:52:47'
System info: host: 'yujun.home', ip: '192.168.1.2', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.9.3', java.version: '1.8.0_05'
Driver info: org.openqa.selenium.chrome.ChromeDriver
Capabilities [{applicationCacheEnabled=false, rotatable=false, chrome={userDataDir=/var/folders/ks/4h1b7nps1vx5712vz12qd3880000gn/T/.org.chromium.Chromium.WmhgSi}, takesHeapSnapshot=true, databaseEnabled=false, handlesAlerts=true, version=35.0.1916.153, platform=MAC, browserConnectionEnabled=false, nativeEvents=true, acceptSslCerts=true, locationContextEnabled=true, webStorageEnabled=true, browserName=chrome, takesScreenshot=true, javascriptEnabled=true, cssSelectorsEnabled=true}]
Session ID: e2f0a757e78c351f2808a6b957c534c5
 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
 at org.openqa.selenium.remote.ErrorHandler.createThrowable(ErrorHandler.java:204)
 at org.openqa.selenium.remote.ErrorHandler.throwIfResponseFailed(ErrorHandler.java:156)
 at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:599)
 at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:352)
 at org.openqa.selenium.remote.RemoteWebDriver.findElementByLinkText(RemoteWebDriver.java:401)
 at org.openqa.selenium.By$ByLinkText.findElement(By.java:242)
 at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:344)
 at com.algocrafts.TicketflyTest.changeLocationUsingSelenium(TicketflyTest.java:26)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.junit.runners.BlockJUnit4ClassRunner.runNotIgnored(BlockJUnit4ClassRunner.java:79)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:71)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
 at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
 at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:74)
 at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:211)
 at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:67)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)


Process finished with exit code 255



The reason is this function is built upon AJAX, it doesn't refresh the entire page, it just repaint the specific area with the new DOM elements. In order to test this feature, we need to add Explicit Wait mechanism to make sure the elements we are looking for appear after certain waiting period, the suitable classes from Selenium are WebDriverWait and FluentWait, using either one, we can rewrite the about test as following,

    @Test
    public void changeLocationUsingSeleniumWithExplicitWait() {
        System.setProperty("webdriver.chrome.driver", "src/main/resources/chrome/chromedriver");
        WebDriver webDriver = new ChromeDriver();
        webDriver.get("http://www.ticketfly.com");
        webDriver.findElement(linkText("change location")).click();
        WebDriverWait wait = new WebDriverWait(webDriver, 5);
        WebElement canada = wait.until(new Function<WebDriver, WebElement>() {
            @Override
            public WebElement apply(WebDriver webDriver) {
                return webDriver.findElement(linkText("CANADA"));
            }
        });
        canada.click();
        WebElement allCanada = wait.until(new Function<WebDriver, WebElement>() {
            @Override
            public WebElement apply(WebDriver webDriver) {
                return webDriver.findElement(linkText("All Canada"));
            }
        });
        allCanada.click();
        assertEquals("Canada", 
                     webDriver.findElement(By.className("tools-location"))
                              .findElement(By.tagName("a"))
                              .findElement(By.tagName("strong"))
                              .getText());
    }



When you run it, it passes.

This test become very verbose when we try to address some common concerns such as wait, it repeats many similar but not exactly same codes. It takes longer time for other developer to read the code and understand what it is doing. We can use framework approach to clean up the code, let us compare the following test written using Selenium Capsules,

    @Test
    public void changeLocationUsingBrowser() {
        Browser browser = CHROME;
        browser.get("http://www.ticketfly.com");
        browser.link(CHANGE_LOCATION).click();
        browser.link(CANADA).click();
        browser.link(ALL_CANADA).click();

        assertEquals("Canada",  Locators.<AbstractPage>element(TOOLS_LOCATION)
                .and(element(A))
                .and(element(STRONG))
                .and(TEXT).locate(new Page(browser)));
    }


It is much cleaner if you write code using framework approach since framework handles many tedious common concerns such as wait and see.

The test can be further cleaned by introducing a Page Object,

    @Test
    public void changeLocation() {
        TicketflyHomePage page = new TicketflyHomePage(CHROME);
        page.open();
        page.changeLocation(CANADA, ALL_CANADA);

        assertEquals("Canada", page.currentLocation());
    }


Even I am impressed by my own work. In case you wonder which class handles the Explicit Wait, it is the ElementLocator class in Selenium Capsules, it calls the untilFound method of Searchable interface as described here.

public class ElementLocator<Where extends Searchable<Where>>
        extends Locators<Where, Element> {

    public ElementLocator(Supplier<By> selector) {
        super((Where where) -> where.untilFound(selector));
    }
}


Unlike the original findElement method in SearchContext of the Selenium API, two new methods are introduced to handle different situations,
  • 1. If an element may appear or may not, use tryElement, it returns null if not found.
  • 2. If an element will appear after sometime since it is AJAX managed DOM element, use untilFound method which is explicitly waiting for the element to appear.


  •     /**
         * 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(Supplier<By> by) {
            try {
                return findElement(by.get());
            } catch (NoSuchElementException e) {
                return null;
            }
        }
    
        /**
         * Find the first element until timeout then throw NoSuchElementException
         *
         * @param by selector
         * @return the first element or throw NoSuchElementException
         */
        default public Element untilFound(Supplier<By> by) {
            return until((Where page) -> findElement(by.get()));
        }
    

    Thursday, June 12, 2014

    handle alert and confirm

    The following two tests demonstrate how to click "OK" on a confirm message box, the first method uses Selenium directly, the second one uses Selenium Capsules framework,

    
    public class ConfirmTest {
    
        @Test
        public void clickConfirmUsingSelenium() {
            System.setProperty("webdriver.chrome.driver", "src/main/resources/chrome/chromedriver");
            WebDriver webDriver = new ChromeDriver();
            webDriver.get("http://localhost:63342/seleniumcapsules/html/upload.html");
            webDriver.findElement(cssSelector("input[value='Need Confirm']")).click();
            webDriver.switchTo().alert().accept();
        }
    
        @Test
        public void clickConfirm() {
            Page page = CHROME.load("http://localhost:63342/seleniumcapsules/html/upload.html");
            page.button(NEED_CONIRM).click();
            page.accept();
        }
    }
    

    Sunday, June 8, 2014

    upload file

    Some applications may require user to upload the file and since it is part of the workflow, without it, the test can be executed end to end, so it is necessary to add the support for file upload.

    Actually file upload is simpler than you think, even it appears like you have to browser the folder to find the file, but it is not a case when you do test automation. The browsing part of the function of the browser, it is out of your control so you don't need to test that part. As tester, we only test the part we are responsible for, for example, after you choose the file, click the button and the file should be sent to server and process by the application. So we can use put the file under test source folder and use Java File to locate it and convert the file into a file and use it for the testing.





    We can put the file to be uploaded inside /test/resources/upload/ folder and refer it in the test.



    The button doesn't submit the form, it just alert a message instead, so the last part of the test is to accept the alert.

    The test can be managed by Spring as well,

    locating

    I found out one commonality between all form elements,

    1. They all have these two instance fields, Where and Locator;
    2. They all call locator.locate(where) before taking other actions.

    To use one word to describe this activity of the locator, it is "locating"

    Thus this class Locating is introduced into Selenium Capsules framework,
    public class Locating<Where extends Searchable<Where>, What> {
    
        protected final Where where;
        protected final Locator<Where, What> locator;
    
        /**
         * Constructor of the Locating.
         *
         * @param where   where
         * @param locator locator
         */
        public Locating(Where where, Locator<Where, What> locator) {
            this.where = where;
            this.locator = locator;
        }
    
        public What locate() {
            return locator.locate(where);
        }
    }
    


    And now Input is
    public class Input<Where extends Searchable<Where>> extends Locating<Where, Element> {
    
        public static final Logger log = getLogger(Input.class);
    
        /**
         * Constructor of the input field.
         *
         * @param where    where
         * @param selector selector
         */
        public Input(Where where, Supplier<By> selector) {
            super(where, Locators.<Where>tryElement(selector));
        }
    
        /**
         * the value of input field, for example, "good" will be return
         * <p>
         * String value = page.get(() -> By.name("status"))
         * <p>
         * <input name="status" value="good"/>
         *
         * @return the value of the input
         */
        public String getValue() {
            final Retry retry = new Retry(5, 1, SECONDS);
            try {
                retry.attempt(() -> {
                    log.info("{}", retry);
                    Element element = locate();
                    return VALUE.locate(element);
                });
            } catch (Exception e) {
                log.info("Failed to read text", e);
            }
            return null;
        }
    
        /**
         * set the value of input field, for example,
         * <p>
         * after,
         * page.set(() -> By.name("status"), "good");
         * <p>
         * it will be,
         * <input name="status" value="good"/>
         *
         * @param value the value to set
         */
    
        public void put(final Object value) {
            String string = value.toString();
            final Retry retry = new Retry(5, 1, SECONDS);
            try {
                retry.attempt(() -> {
                    log.info("{}", retry);
                    Element element = locate();
                    element.clear();
                    element.sendKeys(string);
                    if (VALUE.and(new IsStringEqual(string)).test(element)) {
                        retry.off();
                    }
                    return null;
    
                });
            } catch (Exception e) {
                log.info("Failed to set text {}", string);
            }
        }
    
        /**
         * Test the autocomplete function for the input by given value, click the element
         * on the suggestion list which matches value parameter.
         * <p>
         * Please refer "http://seleniumcapsules.blogspot.com/2014/05/by-xpath.html"
         *
         * @param value   value
         * @param locator locator
         */
        public void autocomplete(Object value, Locator<Where, Element> locator) {
            Element element = locate();
            element.clear();
            Element suggestion;
            for (char c : value.toString().toCharArray()) {
                element.sendKeys(String.valueOf(c));
                suggestion = locator.locate(where);
                if (suggestion != null) {
                    suggestion.click();
                    return;
                }
            }
            suggestion = where.until(locator);
            if (suggestion != null) {
                suggestion.click();
            }
        }
    }
    
    


    Checkbox became,

    public class Checkbox<Where extends Searchable<Where>> extends Locating<Where, Element> {
    
        /**
         * Constructor of the checkbox.
         *
         * @param where    the place the checkbox can be found
         * @param selector the selector that leads to the checkbox
         */
        public Checkbox(final Where where, Supplier<By> selector) {
            super(where, element(selector));
        }
    
        /**
         * Change the checkbox according to the value parameter
         *
         * @param value true or false
         */
        public void setValue(boolean value) {
            Element checkbox = locate();
            if (checkbox != null && checkbox.isSelected() != value) {
                checkbox.click();
            }
        }
    
        /**
         * @return whether the checkbox is checked or not
         */
        public boolean isChecked() {
            return CHECKED.and(TRUE).test(locate());
        }
    }
    
    


    Radio became,

    public class RadioButton<Where extends Searchable<Where>> extends Locating<Where, Stream&tl;Element>> {
    
        /**
         * Constructor this radio button.
         *
         * @param where    where
         * @param selector selector
         */
        public RadioButton(Where where, Supplier<By> selector) {
            super(where, elements(selector));
        }
    
        /**
         * @param value value to set
         */
        public void setValue(Object value) {
            new FirstMatch<>(DISPLAYED.and(VALUE.and(new IsStringEqual(value))))
                    .and(CLICK_IF_NOT_NULL)
                    .locate(locate());
        }
    
        /**
         * @return the value of the select radio
         */
        public String getValue() {
            return new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE)))
                    .and(VALUE)
                    .locate(locate());
        }
    }
    
    


    There no longer have these two instance fields,

    protected final Where where;
    protected final Locator locator;


    And locator.locate(where) became locate(). There is change in the function call chain as well, radioButtonGroup used to be the first function in the chain and now it is FirstMatch, after radioButtonGroup became the locator variable in the super class and locator.locate(where) became locate(), to illustrate the functional transformation, the following three function calls have the same effect.
    
            radioButtonGroup
                    .and(new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE))))
                    .and(VALUE)
                    .locate(where);
    
            new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE)))
                    .and(VALUE)
                    .locate(radioButtonGroup.locate(where));
    
           
            VALUE.locate(
                    new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE))).locate(
                          radioButtonGroup.locate(where)));
    
    


    They all are equivalent to this sequential form,

         Stream<Element> radios = radioButtonGroup.locate(where);
         Element radio = new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE))).locate(radios);
         String value = VALUE.locate(radio);
    


    which is exactly same to this raw form, without Selenium Capsules, you can see a lot of Selenium powder.
    
        String value = null;
        List<WebElement> radios = webDriver.findElements(By.name("customFieldDS.customfield_ROW0_value"));
        for (WebElement radio : radios) {
           if (radio.getAttribute("checked").equals("true")) {
               value = radio.getAttribute("value"));
           }
        }