Saturday, December 8, 2012

Floating Elements in WebDriver

One of the tricky parts of using WebDriver is the design philosophy.  The framework is designed to emulate user behavior, but occasionally fails to match normal human intelligence.  I'll be posting a couple of articles on examples of typical problems when using Selenium WebDriver and suggestions for how to overcome them.  In my examples, I'll showing code that works in ChromeDriver (since I work on a Mac, and Firefox doesn't parallelize as well due to ephemeral port exhaustion).

The first problem: floating elements.  Floating banners, footers, advertisements, or information boxes are somewhat common on sites that want to provide a more dynamic or unified interface.  Unfortunately, when a floating element covers a target element that you wish to interact with.  Browser drivers are pretty adept at detecting when an element is off-page.  In these cases, the drivers automatically scrolls to make the element visible (at least, as of v2.26.0 of Selenium)... but only visible enough to click. If the element is below the bottom of the visible window, then it will automatically be scrolled into view at the bottom of the window.  In this case, it can often be obscured behind a floating footer.

If an element is covered by a floating element, a WebDriverException is generated when the driver tries to click on it; generally something like the following:
Element is not clickable at point (355, 389.5). Other element would receive the click: <a class="ui-corner-all" href="http://www.blogger.com/blogger.g?blogID=9129951650727084756" tabindex="-1">...</a>
There are a few possible strategies one could use at this point:
  • Scroll to the given element whenever you encounter the problem.  This has the disadvantage of adding lots of scrolling to your code, with no guarantee that you'll solve for all cases - if you run your test in a smaller window, new scripts might break, for example.
  • Remove floaters before you start testing, using JavaScript or custom versions of the pages.  This method has the disadvantage of altering the system under test.
  • Use a custom extension of WebElement that catches the error and responds appropriately.  This solution can work globally, without altering the system under test, but does mean that you'll need to find a way to provide these elements without manually converting every element that comes out of a PageFactory or WebDriver, by extending WebDriver, for example.
Here's a sample ScrollingRemoteWebElement that shows a possible fix.  In this case, the WebElement responds to the exception by trying to scroll the element to the center of the page, if possible.

public class ScrollingRemoteWebElement extends RemoteWebElement {

    private static final String VISIBILITY_EXCEPTION = "Element is not clickable at point";

    public ScrollingRemoteWebElement(RemoteWebElement initializingElement) {
        super();
        this.setId(initializingElement.getId());
        this.setParent((RemoteWebDriver) initializingElement.getWrappedDriver());
        this.setFileDetector(parent.getFileDetector());
    }

    @Override
    protected Response execute(String command, Map<String, ?> parameters) {
        try {
            return super.execute(command, parameters);
        } catch (WebDriverException webDriverException) {
            if (isVisibilityProblem(webDriverException)) {
                scrollToElement();
                return super.execute(command, parameters);
            } else {
                throw webDriverException;
            }
        }
    }

    private boolean isVisibilityProblem(WebDriverException webDriverException) {
        String message = webDriverException.getMessage();
        return (message != null
                && message.contains(VISIBILITY_EXCEPTION));
    }

    private void scrollToElement() {
        Integer scrollHeight = getHeightPlacingElementInWindowCenter();
        String script = "window.scrollTo(0," + scrollHeight + ")";
        parent.executeScript(script);
    }

    private Integer getHeightPlacingElementInWindowCenter() {
        Dimension windowSize = parent.manage().window().getSize();
        Integer halfWindow = windowSize.getHeight() / 2;
        Integer scrollHeight = this.getLocation().getY() - halfWindow;
        return (scrollHeight > 0) ? scrollHeight : 0;
    }
}

This element could be improved by adding scrolling on the x-axis to center element before retrying the initial command, but this shows the general strategy. It doesn't solve for elements floating in the center of the page, but one hopes that such an annoyingly screen-grabbing element will not be present one's system under test.

As always, I welcome feedback, especially better solutions than those I've scraped together.

Saturday, May 5, 2012

Testing Ajax Pages with WebDriver

WebDriver is quite capable of detecting page reloads, but Ajax calls usually pass unnoticed.  As a result, the success or failure of a test may depend on the speed of one's network connection. These failures are insidious; you might run into exceptions stating that the element couldn't be found, or that an element you just found has gone "stale" and is no longer attached to the dom. The only solution is to wait for the call to complete before continuing test execution.

The easiest solution (and the one most people, including myself, learn to use first) is to insert a utility like the following, and call it explicitly in the test:

  public static void pause(int millisecs) {
    try {
      Thread.sleep(millisecs);
    } catch (InterruptedException e) {
      LOG.debug("Thread sleep interrupted", e);
    }
  }


Fairly quickly, tests methods begin to look something like this:

  uploadPage.uploadPhoto(testPhoto);
  pause(5000);
  uploadPage.selectPhoto(testPhoto);
  pause(2000);
  assertThat(uploadPage.getDisplayedPhoto(), equalTo(testPhoto));

The cost of this method is actually two-fold: explicit pauses always wait for the maximum duration, and each test method has to be "tuned" for the optimal delay. The test suite takes longer to run, and it takes longer to write tests that don't break intermittently.

So here are a few rules of thumb to combat these problems, to keep tests running quickly without wasting iterations tuning individual test methods.

Never double specify a page interaction


The first topic I always discuss when training testers to use Selenium is always some variant of the DRY principle (Don't Repeat Yourself).  This is a common anti-pattern for testers; they're used to performing repetitive tasks with slight variations. Unfortunately, documentation of test cases can numb you to the stultifying effects of using copy and paste.

When testing against code that is still under development (and why would one test code if no one intended to improve it), copying code is particularly dangerous.  A single change in the id used for a field can break every test case in a suite (say, the submit button on the login page).

Every common interaction with a page should use the same code in every test.  This sets up an abstraction layer between the test script and the application under test, such that a single change in the page should trigger a single change in the test code.  The PageObject pattern is my goto choice for abstraction.

How does this apply to testing Ajax pages?  Any interaction with the page that triggers an an Ajax call should be specified once, so that handling call completion detection is also specified once, in the abstraction layer, rather than being added to every test method that references it.

Block when the call is made


Suppose I have a page where a photo is uploaded to a site and added to a list of photos stored on the site.  A test of the upload function might look something like this:

  uploadPage.uploadPhoto(testPhoto);
  uploadPage.selectPhoto(testPhoto);
  assertThat(uploadPage.getDisplayedPhoto(), equalTo(testPhoto));

Without waiting for the upload to finish, the test runner is likely to report an exception in the "selectPhoto" method if the test photo hasn't finished uploading. One's first impulse is to add logic to the "selectPhoto" method to wait for uploads to finish. There's two problems with this:
  1. The logic for waiting is executed even if "selectPhoto" is called independently of the "uploadPhoto" method, slowing execution unnecessarily.
  2. The logic will need to be added to every other method that might be called after "uploadPhoto".
A better solution is to add any waiting logic to the "uploadPhoto" after driver has been instructed to upload the photo, before returning from the method call, blocking test execution until the upload process is completed.

Wait for the expected change


Finally, avoid using statically defined pauses in test execution - your tests will always run as slowly as the slowest environment it's tuned for. Instead, wait for the specific page change that you expect to see when the Ajax call is made. Here's an example:

  void uploadPhoto(testPhoto) {
    Integer sizeBefore = getUploadListSize();
    enterPhotoLocation(testPhoto.getLocation());
    clickUploadButton();
    waitForListToChange(sizeBefore);
  }

  void waitForUploadListSizeToChange(Integer sizeBefore) {
    WebDriverWait wait = new WebDriverWait(driver, 5);
    wait.until(new ExpectedCondition<Boolean>() {
      @Override
      public Boolean apply(final WebDriver webDriver) {
        return sizeBefore != getUploadListSize();
      }
    });
  }

To break down the process:
  1. Capture the condition of the element that is expected to change before triggering an Ajax call (stored here in "sizeBefore").
  2. Trigger the call (in this case, using "clickUploadButton()").
  3. Wait for the condition to change (by waiting, in this case, for size of the list to change).
The WebDriverWait object makes the process of waiting simple, since it handles looping through the check until the maximum time is reached (five seconds in this case). It can also easily be configured to ignore exceptions, change the interval at which the condition is checked, and change what conditions are verified.

This pattern only works if you know (roughly or concretely) what will change as a result of an Ajax call; I'm still looking for a way to verify that processing has completed if the call may or may not have an effect on the page.

Wednesday, January 11, 2012

Extending JUnit Rules






One of the common problems that I run into when trying to debug failed tests is visibility.  Specifically, which test failed, under what conditions.  One habit that makes this process simpler, for me, is logging test names at the start of each test executed.  The only problem with this practice is that it requires a lot of cut+pasting into each individual test... and that's a nuisance.

Tuesday, June 21, 2011

Web App Testing with Selenium and JUnit

I'm presenting the following slide deck tonight at the Seattle Java Users Group.  In my work for Dynacron Group, I test web applications using Selenium automation, written in Java, running using JUnit, through Sauce Labs cloud services.  This slide deck describes some of the challenges and tools I've used in this process.
 


The formatting is a little scrambled from conversion to a google doc.  Fair warning.

Monday, June 13, 2011

Welcome

This is David Drake's professional blog.  I intend to use this space to write about the development work I'm doing on Khaom Academy.  I'll also be writing here about general testing and development topics.

I'm currently a Seattle SDET for Dynacron Group.  I've been in the testing field for the last five years, but I'm also worked as a middle school math teacher, a merchant teller at a bank, a pizza cook and delivery driver, a technical writer, a tutor, a bagger, a janitor, a factory worker, and a day laborer.

Khaom Academy is a game I'm working on as a side project.  It's being built on the Google App Engine, using Java as the primary language.  This requires learning quite a few new-to-me technologies, so it has been slow to start.  These include:
  • Spring MVC
  • Jsp pages, with jstl
  • JDO data storage, with Objectify
I'll talk more about the particular challenges I'm facing along the way, but in the meantime, those interested in starting a project on the App Engine using Java, Maven, and Spring should take a look at Mike's excellent blog which includes a fair amount of information on the subject.