Testing Asynchronous Behavior in JavaScript with Selenium

Full stack browser testing of web applications is awesome. It validates that your application works end-to-end and allows you to check actual user workflows. For the last year and a half, I’ve been using Selenium to test a JavaScript web application we’re developing using Backbone.js.

Unfortunately, web apps that use JavaScript a lot can be challenging to test with Selenium.

Selenium was designed for testing traditional websites and isn’t well equipped out of the box to handle asynchronous behavior in JavaScript apps that might change the page. Even something simple like looking for an element that’s put on the page in response to an AJAX request will fail with Selenium because it has no idea about the internals of your web app. Selenium just knows that the content it’s looking for is not on the page right now.

A Flawed Workaround

The poor man’s solution to this is just to add lots of sleeps into your test code, so that the test waits long enough for any asynchronous behavior to complete. There are various libraries on top of Selenium that will abstract away sleeping into nicer APIs, but they all share the same core disadvantage. Specifically, they all require you (the programmer) to reason about and provide details about your app’s inner working to your tests, because the framework isn’t capable of figuring them out.

In a JavaScript application of any significant complexity, this sort of behavior starts to become impossible to reason about. As a result, you end up with buggy, unreliable tests. Ember and Angular have recognized this and responded by releasing end-to-end testing solutions that have knowledge of the framework internals.

Unfortunately, for anyone doing JavaScript development without those frameworks, these problems still exist. In the application my team has been working on, these problems had been building over the course of the project, and led to frequent frustrations with our acceptance test suite. Some developers had even moved entirely away from using the suite because they didn’t trust it.

Engineering a Real Solution

With all the problems in our test suite before us, our team decided to devote some serious time into finding a solution for writing Selenium tests.

We had a couple of non-starter attempts that tried to communicate specific parts of our application’s state to our Selenium server, but those quickly grew unmaintainable and polluted our application code with testing concerns. We wanted something that we could write once, that would sit transparently on top of our application so we never had to think about it again.

Then, my teammate Ken had an “Aha!” moment. Since the browser (and therefore Selenium) already blocks on synchronous code being executed, and there are only a few well-defined ways to run asynchronous code, we just needed to globally hook into all sources of asynchronous code and wait for those to finish. (It was one of those head bang moments, where you feel pretty dumb for having tried any other way.)

When we thought about it more, there were really only three sources of asynchronous code we needed to handle: AJAX calls, and calls to setTimeout or setInterval. Since we were using jQuery, we were already able to monitor AJAX calls using the jQuery.active property.

setTimeout and setInterval are a little trickier. There is no way to just get a list of currently active timeouts or intervals from the browser, so we actually need to override the implementation to do some monitoring. Fortunately, this is pretty easy, and Mattie and I were able to whip something up in about an hour.

The code looked something like this (we used underscore.js to make some parts more readable):

// Set up container to hold timeout and interval state
window.testing = {
  timeouts: {},
  intervals: []
};

// Make a copy of the original setTimeout function
window._setTimeout = window.setTimeout;
window.setTimeout = function(callback, timeout) {
  // We need a handle to store our timeout under, we can't just use the
  // timeout ID because we don't know it until after we create the timeout
  var handle = _.uniqueId();

  // Call the old setTimeout function
  var timeoutId = window._setTimeout(
    function() {
      // The callback is the function we were originally deferring
      callback();

      // Once a timeout completes, we need to remove our reference to it
      delete window.testing.timeouts[handle];
    },
    timeout
  );

  // Store the id of the timeout we just created so it can be queried
  window.testing.timeouts[handle] = timeoutId;
  return timeoutId;
};

// Make a copy of the original clearTimeout function
window._clearTimeout = window.clearTimeout;
window.clearTimeout = function(timeoutId) {
  // Call the original clearTimeout function to actually clear the timeout
  var returnValue = window._clearTimeout(timeoutId);

  var timeoutToClear;
  // Look over all the timeouts we have stored and find the one with
  // the timeoutID we just passed in
  _.each(window.testing.timeouts, function(storedTimeoutID, handle) {
    if(storedTimeoutID === timeoutId) {
      timeoutToClear = handle;
    }
  });

  // Delete our stored reference to the timeout
  delete window.testing.timeouts[timeoutToClear];

  return returnValue;
};

// Make a copy of the original setInterval function
window._setInterval = window.setInterval;
window.setInterval = function(cb, interval) {
  // Use the original setInterval function to schedule our interval
  var intervalId = window._setInterval(cb, interval);
  // Store the ID returned by the setInterval call
  window.testing.intervals.push(intervalId);
  return intervalId;
};

// Make a copy of the original clearInterval function
window._clearInterval = window.clearInterval;
window.clearInterval = function(intervalId) {
  // Use the original clearInterval function to clear our interval
  var returnValue = window._clearInterval(intervalId);
  // remove the passed interval ID from our list of IDs
  window.testing.intervals = _.without(window.testing.intervals, intervalId);
  return returnValue;
};

Then, from Selenium, we just need to write a JavaScript snippet that checks the state of the three properties we are interested in, something like:

jQuery.active === 0 
&& Object.keys(window.testing.timeouts).length === 0 
&& window.testing.intervals.length === 0;

The important thing is, if all three are clear, it’s impossible for the browser to run more code or change its state until we perform some UI action. Therefore the current state of the application is safe to query for our test.

Results

This technique worked great for us. We no longer had tests erroring out inconsistently because of async behavior running at different times, and we had to put less thought into constructing our tests because everything just worked.

If you are having trouble controlling asynchronous behavior in your automated browser tests, I recommend giving this a shot. Let me know in the comments if you have any success with it or have any different solutions.

Conversation
  • Tommy Cruise says:

    Hi,
    I’m not familiar with Javascript and try to understand your code, so I can implement the same in Java. Just wondering the parameter “window.testing.timeouts” you passed in this line “&& Object.keys(window.testing.timeouts).length === 0″… What is it? where it comes from?

    Thanks
    Tommy

    • Al Scott Al Scott says:

      That parameter come from the top of the first code sample, lines 2 and 3.

      What part of the code are you hoping to write in Java? Most of this code is concerned with mocking out browser primitives so it needs to be javascript so it can run in the web browser.

  • Edwin Veerkamp says:

    Hi,

    I’m a software tester and would like to know how to incorporate the underscore.js and the snippet in a test case, like the one below:

    package com.example.tests;

    import java.util.regex.Pattern;
    import java.util.concurrent.TimeUnit;
    import org.junit.*;
    import static org.junit.Assert.*;
    import static org.hamcrest.CoreMatchers.*;
    import org.openqa.selenium.*;
    import org.openqa.selenium.firefox.FirefoxDriver;
    import org.openqa.selenium.support.ui.Select;

    public class NunlTest {
    private WebDriver driver;
    private String baseUrl;
    private boolean acceptNextAlert = true;
    private StringBuffer verificationErrors = new StringBuffer();

    @Before
    public void setUp() throws Exception {
    driver = new FirefoxDriver();
    baseUrl = “http://www.nu.nl/”;
    driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
    }

    @Test
    public void testNunl() throws Exception {
    driver.get(baseUrl + “/”);
    driver.findElement(By.linkText(“Economie”)).click();
    driver.findElement(By.linkText(“Sport”)).click();
    driver.findElement(By.linkText(“Tech”)).click();
    driver.findElement(By.linkText(“Voorpagina”)).click();
    }

    @After
    public void tearDown() throws Exception {
    driver.quit();
    String verificationErrorString = verificationErrors.toString();
    if (!””.equals(verificationErrorString)) {
    fail(verificationErrorString);
    }
    }

    private boolean isElementPresent(By by) {
    try {
    driver.findElement(by);
    return true;
    } catch (NoSuchElementException e) {
    return false;
    }
    }

    private boolean isAlertPresent() {
    try {
    driver.switchTo().alert();
    return true;
    } catch (NoAlertPresentException e) {
    return false;
    }
    }

    private String closeAlertAndGetItsText() {
    try {
    Alert alert = driver.switchTo().alert();
    String alertText = alert.getText();
    if (acceptNextAlert) {
    alert.accept();
    } else {
    alert.dismiss();
    }
    return alertText;
    } finally {
    acceptNextAlert = true;
    }
    }
    }

    • Al Scott Al Scott says:

      Hi Edwin.

      I had a function in my javascript code that did a webdriver wait using something like the following code:

      JavascriptExecutor js = (JavascriptExecutor) webDriver;
      try {
      return (Boolean) js.executeScript("jQuery.active === 0
      && Object.keys(window.testing.timeouts).length === 0
      && window.testing.intervals.length === 0;"
      );
      } catch (WebDriverException e) {
      webDriver.navigate().refresh();
      return false;
      }

      Once that waiter returned true, I could be resonably sure the page was ready. I also ended up overriding all of the interaction calls like click and sendKeys with implementations that call that waiter function afterwards. That way every UI interaction forces webdriver to wait until the page is ready.

      As far as using underscore — you need to include that library in page of the site you are testing to use these snippets. Alternatively, they could be rewritten to not use underscore.

      • Edwin Veerkamp says:

        Thanks for your quick reply, I will try to get it working

      • Edwin Veerkamp says:

        Often I don’t have access to librayry of the site that I’m testing. Is there an alternative if that’s the case (with or without using underscore)?

  • AJ says:

    So, let’s say if jQuery is disabled or jQuery is not being used. How would you find that all js calls are done? With jQuery we can do easily by checking jQuery.Active==0. Let me know

    • Al Scott Al Scott says:

      It depends on how you are doing AJAX requests. If you are using some other library to manage them, then you will have to look into the documentation for that library to see if it has something equivalent to jQuery.active. If you are just using raw XMLHttpRequest objects, then you will have to add event listener code to your app to maintain such a counter. Have a look at the API here: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress

      Unfortunately, the native interface doesn’t expose any type of global counter, you have to instrument each individual request.

  • Comments are closed.