Running Selenium Tests on Sauce Labs
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:
- I had issues getting the Selenium Plugin for Hudson working. Upgrading the plugin to use Selenium RC 1.0.5 may solve this issue.
- 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.
- 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:
- 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.
- 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"); }