Functional Testing of an
|
A utomated functional testing of an application that has an ExtJS user interface can be tricky, due to Ext's AJAX asynchronism, complex DOMs and randomly-generated default element IDs. However, recently at SMX I wrote a Java framework on top of Selenium that works around these difficulties and allows us to reliably perform functional testing of a large Web application that has an ExtJS user interface. Our test suite has been easy to develop and maintain within our agile process, and has recently proven itself during a major refactoring. In this article I'm going to give you a breakdown of how I designed the framework. I'll assume that you have experience in Java, Selenium, ExtJS and TestNG. Although I won't publish the complete source code, which is the property of SMX, I'll share with you the essential techniques. Take a look the YouTube clip at the right, which shows a simple test case in action on our application under test - creation, verification, update and deletion of a user account. You'll need the Flash plugin in order to view it. In practice we trigger the test suite on a VCS commit, but in this demo I'm just running it from my IDE. UPDATE September 22, 2009: This technique has been applied to testing the Maven repository manager, Sonatype Nexus, which is written on ExtJS! Patrick Lightbody has written an article all about that.OverviewFor easy maintenance, our test code has three layers:
The Document layer contains a Java proxy for each Ext component type that we use, and we assemble these into containment hierarchies that mirror those within the UI. They have various methods that drive a Selenium proxy (a com.thoughtworks.selenium.Selenium) to fire events and queries at the DOM elements generated by their corresponding Ext components. The Action layer provides facade classes providing logical user actions that drive proxies in the Document layer, encapsulating the latter. Whenever we change UI layouts, the API of the Action layer tends to remain largely unchanged. The TestNG classes in the Test layer drive the facades in the Action layer, organising their logical operations into test cases. Thanks to the Actions layer, code at this level tends to be fairly intention-revealing with respect to use cases. Now I'll describe these layers in more detail, working upwards, with a little background on how we arrived at the present design. Document LayerTo interact with DOM elements, test code must fire Selenese commands at them, specifying the target elements with XPaths. A typical path might be: click //div[5]//div[2]/td[@class="foo"] The primary challenge was to determine these XPaths. We could use Selenium IDE to record a manual click, then cut and past the XPath into our test code, however the path would be fragile, where we would need to repeat that process every time the path changes after a UI modification. However, if we know what ID Ext assigns to the element, then the element's XPath is trivial and stable. For example, if the UI has an Ext.Button and we know its ID is 'ext-gen1048', then the path could be something like: //*[@id='ext-gen1048'] However by default, Ext assigns its own randomly-generated Ids. Since they change each time Ext renders the components, recording the XPaths with Selenium IDE and pasting into test code is futile. We could have Ext assign our own non-random IDs, but we would need to share some kind of complicated hierarchical ID schema between the UI and test code, which would create a maintenance bottleneck. Initial Technique: Finding DOM Elements with Structural XPathsInitially, I took a close look at the HTML generated by Ext components and implemented for each component a Java proxy that had a portion of XPath, so that when the proxies were plugged together to mirror the UI hierarchy, the concatenated portions drilled down to resolve the DOM elements. For example, an Ext.Window proxy had a portion like this: //div[starts-with(@id, 'LoginWindow') and starts-with(@class, 'x-window')] Note that we did configure Ext.Windows with IDs. An Ext.Button proxy had a portion like this: //button[text()='Login'] so when the Button proxy is connected as a child of the Window proxy, the absolute XPath to the Ext.Button's DOM element is //div[starts-with(@id, 'LoginWindow') and starts-with(@class, 'x-window')]//button[text()='Login'] The proxy classes shared a com.thoughtworks.selenium.Selenium and provided methods that would delegate to it. For example, the Button proxy had a click method: class Button ... { ... private String getAbsoluteXPath() { return parent.getAbsoluteXPath() + "//button[text()='Login']"; // Concatenate XPaths } public void click() { this.selenium.click( getAbsoluteXPath() ); } ... } One cool benefit was that we could encapsulate work-arounds for browser weirdness within the proxies. For example, some of them had switch statements to select variations of XPaths or events for different target browsers. Internet Explorer 7, for example did not support clicks on Ext tabs correctly, so a special work-around event was encapsulated within our Tab proxy for that target. Theoretically, this technique was supposed to push XPaths down into the proxy layer once and for all, out of sight and mind, but in practice they required constant maintenance. Sometimes they ended up accidentally ambiguous, resolving to the wrong DOM element. Being snaky and complex, they also tended to break whenever the UI code changed or a new release of Ext was used. Internet Explorer 7 often reared it's ugly head, with little failures such as not being able to reliably determine if elements on XPaths were visible or not. With effort, the tests were maintainable, but too much of my time went into fixing broken XPaths. Also, when a test failed I had to laboriously verify a bunch of XPaths before actually raising a ticket against the application. Better Technique: Java Proxies containing JavaScript to Evaluate the IDs of their Ext ComponentsRecall that if we have the ID of a DOM element then we can form an unambiguous XPath that points directly to it. Since we can get the element ID from the corresponding Ext component, the winning technique was to have Java proxies for Ext components that contain portions of JavaScript. When these are plugged together they form absolute JavaScript expressions that they can evaluate through a com.thoughtworks.selenium.Selenium to obtain their Ext components, from which they can then get IDs and voila, have robust XPaths to their component's DOM elements. No need to assign IDs to all those Ext components, just a few easy ones like Windows and Fields. It was tempting to have the proxies drive their Ext components purely through calls to their components' JavaScript functions, but that would not be testing the actual user interface. We do that sparingly, such as when browsers do not support certain Selenium events on certain HTML elements. To synchronise test execution with asynchronously-loading AJAX widgets, the proxies repeatedly attempt to evaluate JavaScript expressions until they either succeed or time out. When a test creates a proxy, it is usually making an implicit assertion that the UI contains the corresponding widget. A test failure due to one of these timeouts therefore tends to imply that the UI is not in the expected state. Sometimes a proxy asserts the absence of an Ext component or something in the DOM by asserting that a repeatedly-executed JavaScript expression, XPath or Selenese command will fail to resolve it within a certain time limit. This gives the target element a chance to disappear. A skeleton of the basic component proxy class is shown below. This is subclassed for each Ext component type. A root proxy gets a com.thoughtworks.selenium.Selenium, which is shared among all sub-proxies for them to fire Selenese commands at their Ext components or DOM elements, and an absolute JavaScript expression to resolve its Ext component. Non-root proxies get a parent proxy, and a relative JavaScript expression that will be concatenated to that of the parent. The non-root proxies get their Selenium from their parents. public class Component { private Component parent; private Selenium selenium; private String expression; /** Makes a proxy for a root Ext component; * selenium - Selenium proxy through which it can fire Selenese commands, * expression - JavaScript that evaluates to the Ext component. */ public Component(Selenium selenium, String expression) { this.parent = null; this.selenium = selenium; this.expression = expression; } /** Makes a proxy for an Ext component that is contained within another; * parent - proxy for the container Ext component, * expression - JavaScript expression that evaluates this proxy's component on that of the container. */ public Component(Component parent, String expression) { this.parent = parent; this.selenium = parent.selenium; this.expression = expression; } /** Returns the ID of the Ext component, found with the proxy's JS expression. This is overridden in some * subclasses for where the expression to get the ID varies. */ public String getId() { return selenium.getEval(this.getExpression() + ".getId()"; } /** Returns an XPath to the Ext component, which contains the ID provided by getId() */ public String getXPath() { return "//*[@id='" + getId() + "']"; } /** Returns the absolute expression that resolves this proxy's Ext component. */ public Expression getExpression() { return (parent != null) ? parent.getExpression() + expression : expression; } //------------------------------------------------------------ // Methods to synchronise with AJAX //------------------------------------------------------------ protected boolean waitUntilExpressionResolves(String expr) { .. } // Returns true as soon as expression evaluates, else false on timeout protected void waitUntilExpressionNotResolves(String expr) { ... } // Returns true as soon as expression fails to resolve, else timeout exception protected String getEval(String expr) { ... } // Evaluates expressions and returns result. protected void eval(String expr) { ... } // Immediately evaluates expression protected void waitForEvalTrue(String expr) { ... } // Returns as soon as expression evals, else throws exception on timeout //----------------------------------------------------------------- // Convenience methods to evaluate properties of the Ext component. //----------------------------------------------------------------- protected String getEvalStringProperty(String name) { ... } protected boolean getEvalPropertyExists(String name) { ... } protected boolean getEvalBooleanProperty(String name) { ... } protected int getEvalIntegerProperty(String name) { ... } protected double getEvalDoubleProperty(String name) { ... } } For the Ext.Window component, we have the proxy type shown below. It has a JavaScript expression that finds the Ext.Window by ID. Ext.Window is one of the components that we do assign our own IDs to. public class Window extends Component { public Window(Selenium selenium, String id) { super(selenium, "Ext.getCmp('" + id + "')"); } public void close() { log(".close()"); getSelenium().click(getXPath() + "//div[contains(@class, 'x-tool-close')]"); } ... } Shown in the proxy above is one method, close, which closes the Ext.Window. That method obtains the XPath to the window's primary DIV element and fires a click through the Selenium at the close button, found relative to the primary DIV element. Recall that the XPath is generated within the Component base class from the ID of the Ext.Window, which is obtained through evaluation of the proxy's JavaScript expression. Note that the click method first logs the action. I prefix the log messages with the path of names of proxy classes leading down to this proxy in the containment hierarchy, which provides a nice trace of actions performed through the proxies. Here's a proxy for an Ext.Button: public class Button extends Component { public Button(Component parent, String text) { super(parent, ".findBy(function(component) {" + "return (component.isXType && component.isXType('button'))" + "&& (component.text && component.text == '" + text + "')" + "})[0]"; } public boolean isEnabled() { return ( ! getBooleanEval("disabled")); } public void click() { log("click()"); waitForEvalTrue(".disabled == false"); // Throws exception on timeout getSelenium().click(getXPath()); } } The proxy has a JavaScript expression to find the Ext.Button within it's container. Note how the click method waits for the Ext.Button to become enabled (if it is disabled) before firing a Selenese “click” at the HTML element. As I mentioned above, logging at proxies is prefixed with the location of the proxy in the hierarchy. This click will log something like: window["new.User"].button["Save"].click() where the name of each path element is the proxy class name with the first letter decapitalised. The test report contains sequences of entries like this, terminated by stacktraces wherever a failure occurs. They can also be quickly scanned to verify test coverage. To further illustrate expressions, the proxy for an Ext.Toolbar.Button is shown below. One of its constructors initialises the proxy as a child of an Ext.Tab proxy. An Ext.Tab holds an Ext.Toolbar.Button in an items array belonging to an Ext.ToolBar child component, so that constructor defines for the proxy a JavaScript expression to find the Ext.Tab accordingly. public class ToolbarButton extends Component { public ToolbarButton(Tab parent, String text) { super(parent, ".getTopToolBar().items.find(function(component) {" + " return (component.isXType && component.isXType('button'))" + " && (component.text && component.text == '" + text + "')" + " })[0]"; } .... } Domain-Specific ProxiesI sub-class the proxy classes to form domain-specific proxies with names that give some affordance to what they represent within the application. In our application there is the “New User” window, in which an administrator can create users. For that window I have the NewUserWindow proxy, which is both the container and factory for child proxies: public class NewUserWindow extends Window { public NewUserWindow(CSelenium selenium) { super(selenium, "new.User"); } public TextField getFirstNameField() { return new TextField(this, "person.firstName"); } public TextField getLastNameField() { return new TextField(this, "person.lastName"); } public TextField getEmailField() { return new TextField(this, "primaryEmailAddress.emailAddress"); } public TextField getPhoneNumberField() { return new TextField(this, "primaryPhoneNumber.contactNumber"); } public CreateButton getSaveButton() { return new Button(this, "Save"); } public Button getCancelButton() { return new Button(this, "Cancel"); } public NewUserWindow populateWith(User user) { getFirstNameField().setValue(user.getFirstName()); getLastNameField().setValue(user.getLastName()); getEmailField().setValue(user.getEMail()); getPhoneNumberField().setValue(user.getPhoneNumber()); return this; } } Note the populateWith method, which populates the window's widgets with the contents of a User data bean. Also, note that we manually specify Ext Ids for our Ext.Windows. Our application also has a launcher across the top of the desktop area, from where one can create resources, such as users. Below is our proxy for the launcher, which extends a Toolbar proxy and provides ToolbarButton proxy: public class DesktopLauncher extends Toolbar { public DesktopLauncher(Selenium selenium) { super(selenium, "Ext.getCmp('desktop-launcher')"); } .... public ToolbarButton getNewUserButton() { return new ToolbarButton(this, "New User"); } .... } Actions LayerThe Actions layer provides a facade of logical actions on top of the Document Proxy Layer, hiding the latter. It is driven by the Test Layer, providing a level of indirection so that the component proxies can be refactored with minimal impact on test code. It also allows logical actions to be reused among different tests. For example, the actions for testing CRUD on one resource can be reused to set up fixtures for CRUD tests on another dependent resource. public class UserExplorerActions { private Selenium selenium; public UserExplorerActions(Selenium selenium) { this.selenium = selenium; } ... public UserExplorerActions create(User user) { (new DesktopLauncher(this.selenium)) .getNewUserButton() .click(); (new NewUserWindow(this.selenium)) .populateWith(user) .getSaveButton() .click(user); return this; } ... } Tests LayerAt this layer we have TestNG classes which organise logical actions provided by the Actions Layer into tests. Here's the user CRUD test, showing just the user creation part that uses the action described above: @TestState(state="userCrud", dependsOnState = "startApplication") public class UserCrudTest extends SMXTestBase { ... @Test(groups="selenium", dependsOnGroups = "system") public void testUserCrud() { User user = new User( ... ); UserActions actions = new UserActions(this.getSelenium()); actions.createUser(user); ... } ... }
ConclusionI've shown you the bare essentials of a testing framework we successfully use at SMX. The framework has evolved somewhat from what I've shown here, but hopefully this will give you some ideas for how to go about Selenium testing ExtJS applications in Java. Please post any questions or comments, and I'll endeavor to refine this article as we go. LinksHere's a few useful resources.
|


how to trick selenium
Hi Lindsay,
first I would like to thank you for sharing the concepts of your Ext testing concepts.
Currently I'm developing a Perl implementation of it, which can be seen at github: http://github.com/mariominati/test-www-selenium-extjs
What I would like to ask you is how you tricked selenium, that using Ext.getCmp is suficcent instead of using window.Ext.getCmp, as the scope is the selenium object intead of the window object. Curently I'm wrapping the code that shall be evaluated in a anonymous function that is been called with the scope of the window object (Can be seen here: http://github.com/mariominati/test-www-selenium-extjs/blob/master/Test-W... ). But that leeds into problems with getting the return value of multi line code.
Greets,
Mario
IE 7 workaround
"Internet Explorer 7, for example did not support clicks on Ext tabs correctly, so a special work-around event was encapsulated within our Tab proxy for that target. "
I'm not sure, if this is what I'm looking for - but can you tell something more about this work-around event? I experience also a lot of problems with IE7 clicks (Selenium clicks being ignored or just not handled), and I hope your solution could help me.
Hi, Does anyone know if there
Hi,
Does anyone know if there is a similar "window.Ext.getCmp" type of a call I can make on a Ext GWT application. I tried this function call but it does not work for a Ext GWT application. Your help will be greatly appreciated.
Thanks.
window.Ext.getCmp
Hi Kal,
This is a job for Firebug - try loading the application into Firefox and inspecting the javascript object graph with the Firebug extension. I use Firebug constantly alongside development to verify these sorts of things. Just drop 'window' into the script execution window and run it, then you can explore the object graph that it evaluates to.
cheers
Ext JS, div id !!
This is a good article but am still not able to figure out following problem.
I have a simple login page written in extension JS in following way:
Ext.onReady(function()
{
Ext.QuickTips.init();
Ext.form.Field.prototype.msgTarget = 'side';
new Ext.Button({
renderTo:'login',
text: 'Login',
width: 800
}).on('click',function()
{
document.forms[0].submit();
});
});
Now when I use selenium testing via browser I get a randomly generated id which when hard coded can very well be used in following way:
selenium.click("ext-gen26");
My concern is how to get reference of this generated id, so my selenium test case in JUnit is not dependent on randomly generated ids from Extension JS?
Your suggestions appreciated.
Regards,
Prashant
Ext JS, div id !!
You could configure the button with an ID - take a look at the ExtJS documentation at http://www.extjs.com/deploy/dev/docs/
Great article.
What a great model for a framework. Thanks so much for sharing your ideas and code examples.
Could you explain how how you arrived at the set of components for which an ID is manually assigned by the developer?
Component proxy design
Thanks Paul. I also work on the UI itself, so I know what all the Ext widgets are and how they plug together, so I just mirrored those more or less in the Java world. So I guess for this technique the tester would need to also be a developer, or at least in close collusion. Does that answer your question?
When I can find a way to abstract it out, I'd like to write another couple more quick articles extensions I've since made to the tests: 1) verification of ExtJS HTTP requests using Javascript hooks and 2) refactors I did on the Actions layer, where it now has a "fluid API" following the Builder pattern - stay tuned!
Time for more thanks!
Hi Lindsay, I used your design as the basis for my own framework! I had to make some adjustments since I am automating an Ext GWT webapp, so the need to identify Ext JS components by id isn't applicable to my situation. Also I am going for a simpler approach where I rely on the developer to provide meaningful id attributes within their Java code. Then I use XPath expression to find what I want. For example if developer has coded a button with id="Foo" I am able to find it using //*[id="foo"]//button. This works because Ext GWT applies the id to a container of the button tag, rather than to the tag itself. So far so good, the simple everyday tags can be dealt with this way, but I know I'm in for a lot of coding to handle more complex widgets such as grids. Oh BTW did I mention I'm coding in C# instead of Java! This is going to be an interesting ride. Anyway, sorry to ramble, but I really wanted to say that the overall design you have presented here is working really well for me and has enabled me to progress more quickly than I ever thought possible. THANKS !!!
Awesome
That's awesome Paul, your success story makes my day. It turns out that the layered pattern I've used is discussed in "Layered Architecture for Test Automation" - http://www.infoq.com/articles/layered-test-automatation ..interesting! Good luck with those gnarlier Ext widgets.
cheers!
Fantastic article.
Fantastic article.


Great article but where can
Great article but where can we find this framework ?
It's an in-house framework...
...so I can't release it, only write about it. I've actually simplified it somewhat in this article, to distill out the core ideas so others can apply them. However, you might be interested in WebDriver, which seems to be doing similar things with Javascript hooks.
This is Awesome Enough
Nice Article, and it has given me nice exposure on ExtJS as I am a newbie to this, thanks for your effort on this.
Thanks,
Vivek
[http://www.developersnippets.com]
Post new comment