Some Context
In past posts, I have shared my experiences with you concerning how best to use Selenium 1 RC (Remote Control) to test web applications, based on my experiences helping teams. I even wrote a bit of framework code that I shared with you that had helped me and others minimize total-cost-of-ownership of Se test suites. Back when Se RC was one of the very few games in town for page-traversal and DOM-traversal, I found it tricky but workable. I had to write a lot of framework code to keep Se 1 RC Java code as clean as I needed it to be. But it all worked, and I could keep costs contained.
Soooo, a couple of years ago, when I tried using Se 2, I ran into several problems. And I was skeptical about how in the world three different web testing frameworks (Selenium, HTMLUnit, and WebDriver) could be successfully married. At that time I could not, in good concience, recommend to my clients that they use Se 2 and WebDriver. I was afraid that Se had become bloatware. I was afraid that the entire tool was nearing end-of-life.
SE PULLS OUT OF A NOSE DIVE
Well, I’ve been using Se 2 and WebDriver (NOT Se 2 RC) recently (in Java), as I work with Josh Kerievsky and others at Industrial Logic to develop a Selenium 2 album and workshop, and I am here to report to you that as far as I can tell, Se 2 / WebDriver rocks. End-to-end testing with tools like Se are still, in my opinion, testing options of last resort, for reasons I covered in another old post (in short, NEVER use Se to test ANYTHING you can test more affordably with another automated testing tool). But Se 2 / WebDriver in its current incarnation (I’m using selenium-server-standalone jar 2.32 as of this writing) is indeed a whole new ballgame, and deserves more attention, more traction, and more respect in the Web App testing world. Let me summarize.
In With The New
Se 2 is a marriage of Selenium, HTMLUnit (which years and years ago invented the notion of PageObjects, very quietly, simply as a matter of good OO practice), and WebDriver (which is very strong at automating browsers natively, from the operating system, in whatever way a particular browser wants to be controlled by a particular OS version). And in my recent experience, many things that used to burn me about Se RC and Se testing generally are now either way easier, or completely fixed in my current version of Se 2.
The OLD WAY: UGLY SE 1 CODE
Let’s see an example. Selenium 1 Remote Control (RC), when I asked it questions, returned me primitives: integers, strings, booleans. These primitives are all about “how to get lots of stuff done, no matter how confusing that is to try to figure out later.” Here is a classic, old-fashioned, crappy Selenium 1 RC test, with Se calls in-line in the test code (Java in this case):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class ClassicFuglySeRCTest private static SeleniumServer seleniumServer; private static DefaultSelenium selenium; protected static final String SELENIUM_SERVER_HOST = "localhost"; protected static final int SELENIUM_SERVER_PORT = 4444; @Before public void setup() { launchSeleniumBrowser(); } @Test public void navigatePages() { selenium.open("http://demo.fatfreecrm.com/login"); selenium.waitForPageToLoad("60000"); selenium.type("css=input[id=authentication_username]", "seleniumpatterns"); selenium.type("css=input[id=authentication_password]", "seleniumpatterns"); selenium.click("css=input[id=authentication_submit]"); selenium.waitForPageToLoad("60000"); assertTrue(selenium.isElementPresent("css=div[id=welcome]")); assertTrue(selenium.isElementPresent("css=div[id=welcome] span[id='welcome_username']")); assertTrue(selenium.isElementPresent("css=div[id=welcome] a[id=jumper]")); assertTrue(selenium.isElementPresent("css=div[id=welcome] a[href='/profile']")); assertTrue(selenium.isElementPresent("css=div[id=welcome] a[href='/logout']")); selenium.click("css=div[id=tabs] a:contains('Tasks')"); selenium.waitForPageToLoad("60000"); assertTrue(selenium.isElementPresent("css=div[id=welcome]")); assertTrue(selenium.isElementPresent("css=div[id=welcome] span[id='welcome_username']")); assertTrue(selenium.isElementPresent("css=div[id=welcome] a[id=jumper]")); assertTrue(selenium.isElementPresent("css=div[id=welcome] a[href='/profile']")); assertTrue(selenium.isElementPresent("css=div[id=welcome] a[href='/logout']")); ... } } |
Few people ever recommended writing Se RC code like this, but too many programmers, testers, and teams wrote (and still write) Selenium RC code exactly this. There are many problems with this code. You cannot easily tell what the code is doing (in terms of the app being tested, much less its problem domain). And most of us in the Se world have seen much worse code than this, sad to say. But the main thing here, for our discussion, is that you cannot tell easily WHAT pages are being traversed, and WHY they are being traversed (for functional testing purposes). You cannot tell WHAT the test is doing. Also, the mechanics of waiting for pages to be available is embedded in the tests. As I’ve covered elsewhere, you can do a lot of things to clean up code like this. And you shouldn’t have to do so much work.
AN OBJECT ORIENTED API
Let’s hear it for a good old-fashioned Object Oriented API, in which, when I ask a framework object a question, I get returned to me a true object that skillfully encapsulates just the behavior and state I require.
These days, I’m mostly writing Se 2 / WebDriver code that looks like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
public class WhenAUserCreatesAnAccount extends BaseFFCRMTest { private static final String ACCOUNT_NAME = "Bob's Bearings and BassOmatics"; private WebElement accountsTab; private WebElement createAccountPanel; @Test public void theyCanVerifyThatTheyCreatedIt() throws Exception { accountsTab = goToAccountsTab(); createAnAccount(ACCOUNT_NAME); verifyAccountCreated(ACCOUNT_NAME); deleteAccount(ACCOUNT_NAME); verifyAccountDeleted(ACCOUNT_NAME); } private void createAnAccount(String accountName) throws Exception { createAccountPanel = openAccountCreationPane(); fillOutCreateAccountPanel(accountName); } private WebElement openAccountCreationPane() { WebElement createAccountPaneOpeningToggle = accountsTab.findElement(By .id("create_account_arrow")); createAccountPaneOpeningToggle.click(); xdriver.assertElementPresent(By.id("create_account_title")); return accountsTab.findElement(By.id("create_account")); } private void fillOutCreateAccountPanel(String accountName) throws Exception { enterNewAccountName(accountName); submitForm(); } private void enterNewAccountName(String accountName) { WebElement accountNameInputField = createAccountPanel.findElement(By .id("account_name")); accountNameInputField.sendKeys(accountName); } private void submitForm() throws Exception { WebElement submit = createAccountPanel.findElement(By .className("buttonbar")); submit.submit(); } private void verifyAccountCreated(String accountName) { searchForAccount(); WebElement accountLink = xdriver.getElementOnceNotStale(By.partialLinkText("BassOmatics")); accountLink.getTagName(); accountLink.click(); driver.findElement(By.id("edit_account_title")); } private void searchForAccount() { WebElement searchBox = driver.findElement(By.id("query")); searchBox.sendKeys(ACCOUNT_NAME); } private void deleteAccount(String accountName) { WebElement deleteAccountLink = driver.findElement(By.linkText("Delete?")); deleteAccountLink.click(); WebElement confirmation = driver.findElement(By.linkText("Yes")); confirmation.click(); } private void verifyAccountDeleted(String accountName) { searchForAccount(); WebElement noResultsFound = driver.findElement(By.id("empty")); xdriver.assertElementVisible(noResultsFound); assertEquals("Couldn't find any accounts matching " + ACCOUNT_NAME + "; please try another query.", noResultsFound.getText()); } } |
No, I am not employing any PageObjects here, because I have not needed to. I did have to home-grow a bit of frameworkie-code to work around occasional StaleElementReferenceExceptions that too-frequently crop up when testing Ajax-rich apps in which jQuery does a lot of attaching, detaching, and re-attaching elements to the DOM. Yes, WebDriver should provide a driver-configuration API for, perhaps, returning only elements that are NOT STALE, since they are useless. This would impose an ambient load on all element-locating findElement(By …) code, so a nice complement would be an explicit as-needed call to something like findElementOnceFresh(By …). Meanwhile, I am using this workaround, adapted from various ideas on the interwebz:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public WebElement getElementOnceNotStale(By locator) { WebElement element = null; long maxTimeInMillis = timeoutInSeconds/1000; long loopWaitTimeInMillis = 500; long elapsedTimeInMillis = 0; do { try { element = driver.findElement(locator); flowContinuesIfNotStale(element); break; } catch (StaleElementReferenceException sere) { // Element is stale; need to wait briefly and retry findElement() } try { Thread.sleep(loopWaitTimeInMillis); } catch (InterruptedException ie) { ie.printStackTrace(); } } while (weHaventTimedOut(maxTimeInMillis, loopWaitTimeInMillis, elapsedTimeInMillis)); return element; } private void flowContinuesIfNotStale(WebElement element) { element.getTagName(); } private boolean weHaventTimedOut(long maxTime, long waitTime, long elapsedTimeInMillis) { return (elapsedTimeInMillis += waitTime) < maxTime; } |
This is illustrative. My point is that in the arms race between web-app-testing frameworks and internet application richness (great gobs of good, bad, and ugly legacy JS and jQuery, much of it un-microtested), workarounds are still inevitable.
But with Se 2, I am writing cleaner code, faster and easier. And so far, I am rolling my own PageObjects much less frequently, since WebElement objects are very handy right out of the box.
More importantly, Same source origin policy workarounds (proxy servers), and other issues resulting from Se 1 driving the browser from within the JS sandbox, are overcome entirely by Se 2 / WebDriver, which drives the browser natively from outside, in whichever way a browser/OS permutation prefers the browser be driven. This is huge.
So I am doing more with method extractions, so far, and less with entire framework classes. Since test code is not production code, and functional test code can be exceptionally expensive to maintain, less code is a very good thing.
I’ll keep you updated. But I’m going to get to know Se 2 / WebDriver really, really well.

