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.

No comments:

Post a Comment