Matt Raible has been building web applications for most of his adult life. He started tinkering with the web before Netscape 1.0 was even released. For the last 16 years, Matt has helped companies adopt open source technologies (Spring, Hibernate, Apache, Struts, Tapestry, Grails) and use them effectively. Matt has been a speaker at many conferences worldwide, including Devoxx, Jfokus, ÜberConf, No Fluff Just Stuff, and a host of others. Matt is a DZone MVB and is not an employee of DZone and has posted 144 posts at DZone. You can read more from them at their website. View Full User Profile

Running Selenium Tests on Sauce Labs

06.07.2010
| 10164 views |
  • submit to reddit

Recently I embarked on a mission to configure my team's Selenium testing process to support multiple browsers. We use Hudson for our continuous integration server. Since our Hudson instance runs on Solaris, testing with Firefox on Solaris didn't seem like a good representation of our clients. Our browser support matrix currently looks as follows:

Platform Browser
Supported  
Windows IE7.x and 8.x, Firefox 2.x and 3.x
Mac Safari 3.x, 4.x
Best Effort  
Windows and Mac Chrome 4.x

At first, I attempted to use Windows VMs to run Selenium tests on IE. This was a solution that didn't work too well. The major reasons it didn't work:

  1. I had issues getting the Selenium Plugin for Hudson working. Upgrading the plugin to use Selenium RC 1.0.5 may solve this issue.
  2. We had some unit tests that failed on Windows. I tried using the Cygpath Plugin for Hudson (which allows you to emulate a Unix environment on Windows), but failed to get it to work.
  3. We quickly realized it might become a maintenance nightmare to keep all the different VMs up-to-date.

Frustrated by these issues, I turned to Sauce Labs. They have a cloud-based model that runs Selenium tests on VMs that point back to your application. They also support many different browser/OS combinations. We asked them about support for OS X and various Windows versions and they indicated that their experience shows browsers are the same across OSes.

I'm writing this article to show you how we've configured our build process to support 1) testing locally and 2) testing on Sauce Labs. In a future post, I hope to write about how to run Selenium tests concurrently for faster execution.

Running Selenium Tests Locally
We use Maven to build our project and run our Selenium tests. Our configuration is very similar to the poms referenced in Integrating Selenium with Maven 2. Basically, we have an "itest" profile that gets invoked when we pass in -Pitest. It downloads/starts Tomcat (using Cargo), deploys our WAR, starts Selenium RC (using the selenium-maven-plugin) and executes JUnit-based tests using the maven-surefire-plugin. All of this configuration is pretty standard and something I've used on many projects over the past several years.

Beyond that, we have a custom BlockJUnit4ClassRunner class that takes screenshots and captures the HTML source for tests that fail.

public class SeleniumJUnitRunner extends BlockJUnit4ClassRunner {
public SeleniumJUnitRunner(Class<?> klass) throws InitializationError {
super(klass);
}

protected Statement methodInvoker(FrameworkMethod method, Object test) {
if (!(test instanceof AbstractSeleniumTestCase)) {
throw new RuntimeException("Only works with AbstractSeleniumTestCase");
}

final AbstractSeleniumTestCase stc = ((AbstractSeleniumTestCase) test);
stc.setDescription(describeChild(method));

return new InvokeMethod(method, test) {
@Override
public void evaluate() throws Throwable {
try {
super.evaluate();
} catch (Throwable throwable) {
stc.takeScreenshot("FAILURE");
stc.captureHtmlSource("FAILURE");
throw throwable;
}
}
};
}
}

To use the functionality SeleniumJUnitRunner provides, we have a parent class for all our tests. This class uses the @RunWith annotation as follows:

@RunWith(SeleniumJUnitRunner.class)
public abstract class AbstractSeleniumTestCase {
// convenience methods
}

This class looks up the Selenium RC Server, the app location and what browser to use based on system properties. If system properties are not set, it has defaults for running locally.

public static String SERVER = System.getProperty("selenium.server");
public static String APP = System.getProperty("selenium.application");
public static String BROWSER = System.getProperty("selenium.browser");

protected Selenium selenium;

@Before
public void setUp() throws Exception {
if (SERVER == null) {
SERVER = "localhost";
}

if (BROWSER == null) {
BROWSER = "*firefox3";
}

if (APP == null) {
APP = "http://localhost:9000";
}

selenium = new DefaultSelenium(SERVER, 4444, BROWSER, APP);
selenium.start("captureNetworkTraffic=true");
selenium.getEval("window.moveTo(1,1); window.resizeTo(1021,737);");
selenium.setTimeout("60000");
}

The system properties are specified as part of the surefire-plugin's configuration. The reason we default them in the above code is so tests can be run from IDEA as well.

<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<systemPropertyVariables>
<selenium.application>${selenium.application}</selenium.application>
<selenium.browser>${selenium.browser}</selenium.browser>
<selenium.server>${selenium.server}</selenium.server>
</systemPropertyVariables>
</configuration>

Running Selenium Tests in the Cloud
To run tests in the cloud, you have to do a bit of setup first. If you're behind a firewall, you'll need to setup SSH tunneling so Sauce Labs can see your machine. You'll also need to setup SSH Tunneling on your Hudson server, but installing/configuring/running locally is usually a good first step. Below are the steps I used to configure Sauce Labs' SSH Tunneling on OS X.

1. Install the Python version in /opt/tools/saucelabs. If you get an error (No local packages or download links found for install) download the egg and run it with:

sudo sh setuptools-0.6c11-py2.6.egg

NOTE: If you get an error (unable to execute gcc-4.2: No such file or directory) when installing pycrypto on OS X, you'll need to install the OS X Developer Tools.

2. Create a /opt/tools/saucelabs/local.sh script with the following in it. You should change the last parameter to use your username (instead of mraible) since Sauce Labs uses unique tunnel names.

python tunnel.py {sauce.username} {sauce.key} localhost 9000:80 mraible.local

3. Start the tunnel by executing local.sh. You should see output similar to the following.

$ sh local.sh 
/System/../Python.framework/../2.6/../twisted/internet/_sslverify.py:5: DeprecationWarning: the md5 module is deprecated; use hashlib instead
import itertools, md5
/System/../Python.framework/../2.6/../twisted/conch/ssh/keys.py:13: DeprecationWarning: the sha module is deprecated; use the hashlib module instead
import sha, md5
Launching tunnel ...
Status: new
Status: booting
Status: running
Tunnel host: ec2-75-101-216-8.compute-1.amazonaws.com
Tunnel ID: 70f15fb59d2e7ebde55a6274ddfa54dd
<sshtunnel.TunnelTransport instance at 0x10217ad88> created
requesting remote forwarding for tunnel 70f15fb59d2e7ebde55a6274ddfa54dd 80=>localhost:9000
accepted remote forwarding for tunnel 70f15fb59d2e7ebde55a6274ddfa54dd 80=>localhost:9000

After setting up the SSH Tunnel, I modified AbstractSeleniumTestCase's setUp() method to allow running tests on Sauce Labs.

@Before
public void setUp() throws Exception {
if (SERVER == null) {
SERVER = "localhost";
}

if (BROWSER == null) {
BROWSER = "*firefox3";
} else if (BROWSER.split(":").length == 3) {
String[] platform = BROWSER.split(":");

String os = platform[0];
String browser = platform[1];

// if Google Chrome, don't use a version #
String version = (platform[1].equals("googlechrome") ? "" : platform[2]);
String printableVersion = ((version.length() > 0) ? " " + platform[2].charAt(0) : "");

String jobName = description.getMethodName() + " [" + browser + printableVersion + "]";

BROWSER = "{\"username\":\"{your-username}\",\"access-key\":\"{your-access-key}\"," +
"\"os\":\"" + platform[0] + "\",\"browser\": \"" + platform[1] + "\"," +
"\"browser-version\":\"" + version + "\"," +
"\"job-name\":\"" + jobName + "\"}";

log.debug("Testing with " + browser + printableVersion + " on " + os);
}

if (APP == null) {
APP = "http://localhost:9000";
}

selenium = new DefaultSelenium(SERVER, 4444, BROWSER, APP);
selenium.start("captureNetworkTraffic=true");
selenium.getEval("window.moveTo(1,1); window.resizeTo(1021,737);");
selenium.setTimeout("60000");
}

After making this change, I was able to run Selenium tests from IDEA using the following steps:

  1. Start Jetty on port 9000 (since that's what the tunnel points to). In IDEA's Maven panel, create a run/debug configuration for jetty:run, click the "Runner" tab and enter "-Djetty.port=9000" in the VM Parameters box.
  2. Right-click on the test to run and create a run/debug configuration. Enter the following in the VM Parameters box. The last two parameters allow skipping the xvfb and Selenium RC startup process.
    -Dselenium.browser="Windows 2003:iexplore:8." -Dselenium.application=mraible.local
    -Dselenium.server=saucelabs.com -Dxvfb.skip=true -Dselenium.server.skip=true

These same parameters can be used if you want to run all tests from the command line:

mvn install -Pitest -Dselenium.browser="Windows 2003:iexplore:8." 
-Dselenium.application=mraible.local -Dselenium.server=saucelabs.com
-Dxvfb.skip=true -Dselenium.server.skip=true -Dcargo.port=9000

To simplify things, we create profiles for the various browsers. For example, below are profiles for IE8 and Firefox 3.6.

<profile>
<id>firefox-win</id>
<properties>
<cargo.port>9000</cargo.port>
<selenium.application>http://${user.name}.local</selenium.application>
<selenium.browser>Windows 2003:firefox:3.6.</selenium.browser>
<selenium.server>saucelabs.com</selenium.server>
<selenium.server.skip>true</selenium.server.skip>
<xvfb.skip>true</xvfb.skip>
</properties>
</profile>
<profile>
<id>ie-win</id>
<properties>
<cargo.port>9000</cargo.port>
<selenium.application>http://${user.name}.local</selenium.application>
<selenium.browser>Windows 2003:iexplore:8.</selenium.browser>
<selenium.server>saucelabs.com</selenium.server>
<selenium.server.skip>true</selenium.server.skip>
<xvfb.skip>true</xvfb.skip>
</properties>
</profile>

Issues
Since we've started using Sauce Labs, we've run into a number of issues. Some of these are Selenium-related and some are simply things we learned since we started testing on multiple browsers.

  • SSH Tunnels Keep Restarting This happens on our Hudson server that runs the tunnels as a service. This seems to happen daily and screws up our Hudson results because builds fail.
  • XPath vs. CSS Selectors One of the first things we noticed was that our IE tests were 2-3 times slower than the same tests on Firefox. We discovered this is because Internet Explorer has a very slow XPath engine. To fix this issue, it's recommended that ids or CSS Selectors be used whenever trying to locate elements. For more information on CSS Selectors and Selenium, see CSS Selectors in Selenium Demystified. To test CSS Selectors, I found Firefinder to be a very useful Firefox plugin. Note that many pseudo elements won't work in IE.
  • IE7 fails to initialize on Sauce Labs There's no errors in our JUnit reports, so we're not sure what's causing this. It could very well be bugs in our code/configuration, but IE8 works fine.
  • The Job Names on Sauce Labs don't get set correctly and often results in duplicate job names. This could certainly be related to my code. Finding videos that show failed tests is difficult when the job names aren't set correctly.
  • It would be slick if you could download the video of a failed test, similar to what we do by taking screenshots.
  • Google Chrome works on Sauce Labs, but I'm unable to get it working locally (on Windows or OS X). This seems to be a Selenium issue.
  • Safari 4 works, but when it fails, the screenshot shows a Safari can't find the file error. Since there's no real error to debug, it's difficult to figure out why the test fails. Since Safari 4 is not listed on platforms supported by Selenium, I'm unsure how to fix this.

Overall, Sauce Labs seems to work pretty well. However, in the process of messing with Hudson, build agents and Selenium infrastructure, it's become readily apparent that we need a team member to devote their full-attention to it. Having a developer or two work on it every now-and-then is inefficient, especially when we're still in the process of ironing everything out and making it all stable.

If you have any tips on how you've solved issues with Sauce Labs (ssh tunnels, IE7) or Selenium (Safari 4, Google Chrome), I'd love to hear them. I'm also interested to hear from anyone with experience running Selenium tests concurrently (locally or in the cloud).

Update: I discovered a bug in my AbstractSeleniumTest's setUp() method where job names weren't being set correctly. I've since changed the code in this class to the following:

private static String browser, printableVersion;

@BeforeClass
public static void parseBrowser() {

if (BROWSER == null) {
BROWSER = "*firefox3";
} else if (BROWSER.split(":").length == 3) {
String[] platform = BROWSER.split(":");

String os = platform[0];
browser = platform[1];

// if Google Chrome, don't use a version #
String version = (platform[1].equals("googlechrome") ? "" : platform[2]);
printableVersion = ((version.length() > 0) ? " " + platform[2].charAt(0) : "");

BROWSER = "{\"username\":\"{your-username}\",\"access-key\":\"{your-access-key}\"," +
"\"os\":\"" + os + "\",\"browser\": \"" + browser + "\"," +
"\"browser-version\":\"" + version + "\", " +
"\"job-name\": \"jobName\"}";
}
}

@Before
public void setUp() throws Exception {
if (SERVER == null) {
SERVER = "localhost";
}

if (APP == null) {
APP = "http://localhost:9000";
}

String seleniumBrowser = BROWSER;
if (BROWSER.startsWith("{")) { // sauce labs
String jobName = description.getMethodName() + " [" + browser + printableVersion + "]";
log.debug("=> Running job: " + jobName);

seleniumBrowser = BROWSER.replace("jobName", jobName);
}

selenium = new DefaultSelenium(SERVER, 4444, seleniumBrowser, APP);
selenium.start("captureNetworkTraffic=true");
selenium.getEval("window.moveTo(1,1); window.resizeTo(1021,737);");
selenium.setTimeout("60000");
}

From http://raibledesigns.com/rd/entry/running_selenium_tests_on_sauce

Published at DZone with permission of Matt Raible, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Michael Eric replied on Wed, 2012/09/26 - 3:46pm

Hi Matt, nice write up! Some points I think can reduce your problems:

  • I'd solve the tunnel issue with the new plugin suggested above. There's no more needs for our python script or any installation if your just use that.
  • Try using iexploreproxy instead of iexplore for IE7. This could be caused by problems in the way your app interacts with IE HTA, the special "heightened privilege" mode that selenium uses to run IE by default.
debian

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.