GWT Application Testing with Selenium

One of the challenges in testing a GWT application using a tool like Selenium, is to be able to accurately tell when all the asynchronous requests have finished and that your application is in a state you expect. I’ve searched around the internet and didn’t find any useful solutions on this topic so I’ll share my implementation.

Defining Our Ajax Hook

Since GWT is all about Ajax, we need to inject some Ajax hooks into our production web application. I’ll create a new Semaphore that will be included as part of the javascript that gets loaded into the html of the GWT application. This will be our door in.

semaphore.js:

var IN_FLIGHT_AJAX = IN_FLIGHT_AJAX || function() {
	// private variable bound through closure
	var activeCallCount = 1;

	return {
		callsInFlight: function() {
			return activeCallCount;
		},
		decrementCallsInFlight: function() {
			activeCallCount--;
		},
		incrementCallsInFlight: function() {
			activeCallCount++;
		}
	}
}();

Notice how I’ve initialized the counter to be 1. This is important because the GWT bootstrap process in itself can take a bit of time. So, at a minimum we have to wait until the module is fully loaded. Otherwise, you will get the occasional timing/synchronization failure during test runs.

Bridge Between GWT and Our Testing Hook

I’ll create a JavaScript Native Interface (JSNI) Java class to provide the ability to manipulate our semaphore from within our GWT application.

InFlightAjaxCallMarker.java

public class InflightAjaxCallMarker {

	private boolean hasStartedCall = false;
	private boolean hasEndedCall = false;

	public InflightAjaxCallMarker() {
		super();
	}

	public void startCall() {
		if (!hasStartedCall) {
			incrementCallsInFlight();
			hasStartedCall = true;
		}
	}

	public void endCall() {
		if (!hasEndedCall) {
			decrementCallsInFlight();
			hasEndedCall = true;
		}
	}

	private native void decrementCallsInFlight() /*-{
		try {
			if (($wnd.IN_FLIGHT_AJAX) && (typeof $wnd.IN_FLIGHT_AJAX.decrementCallsInFlight === "function")) {
				$wnd.IN_FLIGHT_AJAX.decrementCallsInFlight();
			}
		} catch (ex) {
			// Do error handling
		}
	}-*/;

	private native void incrementCallsInFlight() /*-{
		try {
			if (($wnd.IN_FLIGHT_AJAX) && (typeof $wnd.IN_FLIGHT_AJAX.incrementCallsInFlight === "function")) {
				$wnd.IN_FLIGHT_AJAX.incrementCallsInFlight();
			}
		} catch (ex) {
			// Do error handling
		}
	}-*/;
}

Marking Our Module Loaded

As I mentioned earlier, we want to wait for our module to be fully loaded. This can be accomplished by adding a call to the end of onModuleLoad().

public void onModuleLoad() {
		... perform some stuff
		new InflightAjaxCallMarker().endCall();
	}

Marking Our Ajax Calls

The easiest way to mark all our Ajax calls is to go through the AsyncCallback interface. Since all our asynchronous interactions require a callback, we can create a callback template to perform our marking. From this point onward, developers will create new AsyncCallbackAdapter classes instead of AsyncCallback.

AsyncCallbackAdapter.java

public abstract class AsyncCallbackAdapter<T> implements AsyncCallback<T> {

	private InflightAjaxCallMarker inflightAjaxCallMarker;

	public AsyncCallbackAdapter() {
		this.inflightAjaxCallMarker = new InflightAjaxCallMarker();
		this.inflightAjaxCallMarker.startCall();
	}

	@Override
	public final void onFailure(Throwable caught) {
		try {
			onFailureBody(caught);
		} finally {
			inflightAjaxCallMarker.endCall();
		}
	}

	protected abstract void onFailureBody(Throwable caught);

	@Override
	public final void onSuccess(T result) {
		try {
			onSuccessBody(result);
		} finally {
			inflightAjaxCallMarker.endCall();
		}
	}

	protected abstract void onSuccessBody(T result);

}

Getting Selenium to Wait

Finally, we will create a Selenium function to test our Ajax status.

String script = "selenium.isAjaxComplete = function() {
	var appWindow = selenium.browserbot.getCurrentWindow();
	if ((appWindow.IN_FLIGHT_AJAX) && (typeof appWindow.IN_FLIGHT_AJAX.callsInFlight === 'function')) {
		return (appWindow.IN_FLIGHT_AJAX.callsInFlight() === 0);
	}
	return false;
}";
selenium.addScript(script, scriptTagId);

Now, we can force Selenium to wait for Ajax calls to finish whenever we need.

selenium.waitForCondition("selenium.isAjaxComplete();", timeoutValue);

How Does the Semaphore Work With Multi-Threaded Rendering?

There are a few things that need to be clarified before I start explaining about the workings of the semaphore.

  1. The semaphore needs be included on each html page.
  2. Javascript doesn’t have threads. In all browser implementations, there is only one thread of execution for javascript in each browser window/tab.
  3. Each browser will also have a native timer thread and any number of native threads to do asynchronous XMLHttpRequest (XHR) calls. These native threads are not accessible nor can we count on them in javascript. When an asynchronous response returns from the server, it is the browser implementation that places the callback code onto the javascript execution stack. No assumption should be made about the order of execution.
  4. In javascript, there are only 2 variable scopes: global, and function.

Now, on to the details.

Our semaphore is created in the global (top window) scope. If an existing object already exists, it will use that existing object. Otherwise, it will execute an anonymous function that returns us a new object.

var IN_FLIGHT_AJAX = IN_FLIGHT_AJAX || function() {
   ...
    return {
        ...
    }
}();

Our private counter is only accessible inside our semaphore object because we defined it using function scope. Outside of the function, activeCallCount cannot be modified and this validates the semaphore. Through the “magic” of closure, we can provide accessors to our counter even after the function has returned. Since javascript is single threaded execution, we are guaranteed to serialize the increment and decrement of the counter.

// private variable bound through closure
    var activeCallCount = 1;

With multiple browser windows or tabs, there will be a single semaphore loaded and associated within each window/tab. This works in conjunction with the fact that each window/tab has its own global environment. The asynchronous XHR calls in each window/tab will only affect its associated semaphore, and that’s what we want.

Conclusion

This is a comprehensive GWT testing solution for use with tools like Selenium. The semaphore implementation can be used with any Javascript framework, not just GWT. The JSNI bridge along with the AsyncCallback template is the key to providing a seamless integration with GWT applications. This approach to waiting for Ajax calls to finish will deter developers from writing brittle web tests that depend on specific pieces of text or UI elements. The drawback to this implementation is that production code will contain small amounts of testing hooks.

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply