Testing Nexus With Selenium: A Lesson in Complex UI Testing (Part 2)

October 01, 2009 By Brian Fox

8 minute read time

By Patrick Lightbody

Part one

Specific Challenges with Nexus and ExtJS

When applying these rules specifically to Nexus - and its underlying UI framework, ExtJS - there are a few issues that actually work against the best practices when creating Selenium locators. For example, ExtJS automatically generates ID attributes for the underlying HTML elements that it creates when using the various UI components such as a popup dialog box, form container, or sliding "drawer" which is present on the left hand side of Nexus.

These generated IDs look like ext-419, where 419 is a somewhat unpredictable/random number that is based on browser timing issues and page construction events that may change from page load to page load. What that means is that if you are recording a test, you might find these IDs referenced in your script, but if you were to play back the test it might not work the next time around. This is clearly a problem - our "best practice" is no longer working for us!
The other issue that we encountered was dealing with components that are hidden or otherwise not visible. ExtJS has a nice design that only generates the HTML elements for a component if it needs to be displayed. This "lazy loading" approach improves performance on the browser itself, but it also makes your Selenium tests a little more difficult to write. This is because it's sometimes hard to tell whether you should use an "element present" check or an "element visible" check and often end up needing to use both to properly synchronize your tests.


Working with ExtJS and the Page Object Pattern

So given these problems with ExtJS, what can be done to make test creation easier? Fortunately there's a great article that discusses the very approach we used when developing the Nexus test case system. In short, the article suggests creating Java classes that represent the logical page-level components (ie: ChangePasswordWindow) that generate dynamic locators based on the ExtJS API.
For example, in Nexus the window for changing your password is given an ID of "change-password-window", but the individual form fields are not given any ID that we could easily use in our locators. Fortunately, ExtJS provides a very nice API for first locating the window and then locating the text field relative to the window:

var cpWindow = Ext.getCmp('change-password-window');
var textField = cpWindow.findBy(function(c) {
    return c.fieldLabel == 'Current Password';
})[0];

This code uses the Ext.getCmp() function, which returns a component located somewhere on the page by ID. From there, we then use the findBy() function, which takes in an anonymous function that we use to filter down all components in the cpWindow and extract out only the one we're interested in.
What this means is that we could write a Selenium expression that types in to that field using the following expression:

document=Ext.getCmp('change-password-window').findBy(function(c) { return c.fieldLabel == 'Current Password' })[0]
While that would work, clearly it's a lot of text and wouldn't be very maintainable. But what we can do is extract these various bits in to Java classes that will be smart enough to abstract all this complexity away. Our Selenium test, which is now written in Java, will look very simple:

public class ChangePasswordTest extends SeleniumTest {
    @Test
    public void changePasswordSuccess() {
        main.clickLogin()

                .populate(User.ADMIN)
                .loginExpectingSuccess();

        ChangePasswordWindow window = main.securityPanel().clickChangePassword();

        // ...
        
        PasswordChangedWindow passwordChangedWindow = window

                .populate("password", "newPassword", "newPassword")
                .changePasswordExpectingSuccess();

        passwordChangedWindow.clickOk();
    }
}

What we've done is abstract out and conceptualize all the major components of the Nexus UI in to simple, reusable Java classes. First, we can see that ChangePasswordTest extends SeleniumTest. SeleniumTest is a class we created that automatically sets up Selenium according to a few different infrastructure requirements and settings (more on that later). Most importantly, it creates a protected "main" variable that we reference at the start of this test.

The main variable is a MainPage object, which is designed to represent all the top-level interactions a user can do with Nexus, such as click the "login" link in the upper right-hand corner. This has been abstracted out so you can simply call clickLogin(), which returns a LoginWindow object, which in turn can be populated with the login credentials for the admin, and then finally logged in and told to expect success. If the login fails, the LoginWindow code is designed to throw an exception and fail the test.
Next, we navigate to the security panel on the left-hand side of the Nexus UI with securityPanel() and then click the "change password" link with clickChangePassword(), which returns a ChangePasswordWindow. With a handle to this window, we can populate the embedded form with the populate() function and then finally click the button that saves the changes. 

The entire result is a very clean test, but unless you have seen an example of one of these underlying objects, it may look like a bunch of magic. If you're curious, you can examine the entire source here. Let's take a look at the ChangePasswordWindow.java source:

public class ChangePasswordWindow extends Window {
    private TextField currentPassword;
    private TextField newPassword;
    private TextField confirmPassword;
    private Button button;

    public ChangePasswordWindow(Selenium selenium) {
        super(selenium, "window.Ext.getCmp('change-password-window')");
        currentPassword = new TextField(this, ".findBy(function(c) { return c.fieldLabel == 'Current Password' })[0]");
        newPassword = new TextField(this, ".findBy(function(c) { return c.fieldLabel == 'New Password' })[0]");
        confirmPassword = new TextField(this, ".findBy(function(c) { return c.fieldLabel == 'Confirm Password' })[0]");
        button = new Button(selenium, "window.Ext.getCmp('change-password-button')");
    }
    public ChangePasswordWindow populate(String current, String newPass, String confirm) {

        currentPassword.type(current);

        newPassword.type(newPass);
        confirmPassword.type(confirm);

        return this;

    }

    public PasswordChangedWindow changePasswordExpectingSuccess() {
        button.click();
        waitForHidden();
        return new PasswordChangedWindow(selenium);
    }
}
A few important notes: ChangePasswordWindow extends Window, another class we've created that provides access to standard capabilities that ExtJS exposes in any window component. Window itself extends a Component class, which also exposes generic functionality that any ExtJS component can provide, such as waiting for the component to be hidden with the waitForHidden() method.
In the constructor you can also see that we define the JavaScript expression that gets a handle to the window itself, but also create four additional objects that represent the critical form elements we will want to interact with. The key thing to note is that some of these components can take in another component (the ChangePasswordWindow) while others take in the Selenium object itself. 
The difference here is that components that take in another component can have a partial locator expression, since it will be strung together using the parent component's locator string. Alternatively, those that are given a Selenium object directly must be given a component locator that is fully qualified and standalone. In this example, you can see uses of both, since the button had an ID that we could reference but the text fields did not.
This approach is commonly referred to as the Page Object Pattern and is becoming increasingly popular among the Selenium community as a way to write tests that will withstand the test of time. But even a well designed test is only half the battle: if the underlying application state isn't reliably recreated each time the test runs, the tests will likely have difficulty passing consistently.

Tags: Sonatype Says

Written by Brian Fox

Brian Fox is a software developer, innovator and entrepreneur. He is an active contributor within the open source development community, most prominently as a member of the Apache Software Foundation and former Chair of the Apache Maven project. As the CTO and co-founder of Sonatype, he is focused on building a platform for developers and DevOps professionals to build high-quality, secure applications with open source components.