Selenium 2 Chrome Switches via ChromeDriver

 

Ok, I’m loving the combination of Selenium 2 with Twist.  This is especially true when using the new WebDriver implementation from the Chromium team.  It’s blisteringly fast, and is helping shorten the CI feedback loop even more (very important to our teams!).  Thanks to everyone involved in delivering this to the world.  However, one thing stumped me when working with the new chromedriver.exe set up… How do I pass command line switches on to Chrome browser instance being launched?  I worked it out from looking in the Chromium source for tests (always a good place to start for gaining an understanding).  Here is the solution below (expanding on some code from one of my previous posts in my WebDriverFactory).

WebDriverEventListener eventListener = new LoggingWebDriverEventListener();

service = new ChromeDriverService.Builder()
.usingChromeDriverExecutable(new File("./libs/chromedriver_win32_13.0.775.0/chromedriver.exe"))
.usingAnyFreePort()
.build();
service.start();

DesiredCapabilities capabilities = DesiredCapabilities.chrome();
String[] switches = {"start-maximized","remote-debugging-port=9222"};
capabilities.setCapability("chrome.switches", switches);

selenium = new EventFiringWebDriver(new RemoteWebDriver(service.getUrl(), capabilities)).register(eventListener);

It’s not doing anything exciting I know, but I thought this post might help out anyone else who might be struggling with finding the way to achieve the same thing.

I seriously want to work out a way of hooking nicely in to the remote debugger interface over Web Inspector Protocol next.  This little gem was the first step for integrating my efforts with Selenium 2, and therefore it’s of value to me (and as a by product… may be to you).  Any way, on with working out how to handle web sockets to get this WIP integration idea of mine some legs.

Logging Selenium 2 Events in Twist

Limitations of using custom drivers

I’ve been using Selenium 2 quite heavily with Twist over the last couple of weeks, and a few things have been bothering me.  These things are mainly issues around not using a tightly integrated driver like Selenium 1 and Sahi.  For those drivers, ThoughtWorks have put in a great deal of effort in to handling configuration, logging, and various other niceties.  So not being one to let that hold our teams back from those things that are of value to them, I’ve started having a crack at spiking out a few solutions.  The first one being the handling of useful logging from the Selenium 2 driver.  Here’s the output from that spike.

Implementing a WebDriver event listener

It turns out that the Selenium 2 team have made life easy from the start on this one.  There is an listener interface that can be implemented against for hooking it to useful driver state information at key event firing points.  At this point, I just want to get up and running to have a play with what could be done.  So I just created a new class that implemented stubs against each method (which was required anyway as it was an abstract interface).  So that looked a little something like below.

package common.driver;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.events.WebDriverEventListener;

public class MyWebDriverEventListener implements WebDriverEventListener {

	public void afterChangeValueOf(WebElement element, WebDriver selenium) {}
	public void afterClickOn(WebElement element, WebDriver selenium) {}
	public void afterFindBy(By by, WebElement element, WebDriver selenium) {}
	public void afterNavigateBack(WebDriver selenium) {}
	public void afterNavigateForward(WebDriver selenium) {}
	public void afterNavigateTo(String url, WebDriver selenium) {}
	public void afterScript(String script, WebDriver selenium) {}
	public void beforeChangeValueOf(WebElement element, WebDriver selenium) {}
	public void beforeClickOn(WebElement element, WebDriver selenium) {}
	public void beforeFindBy(By by, WebElement element, WebDriver selenium) {}
	public void beforeNavigateBack(WebDriver selenium) {}
	public void beforeNavigateForward(WebDriver selenium) {}
	public void beforeNavigateTo(String url, WebDriver selenium) {}
	public void beforeScript(String script, WebDriver selenium) {}
	public void onException(Throwable error, WebDriver selenium) {}

}

Hooking the event listener in to the driver

We already had a driver factory for managing Selenium 2 via Spring.  So it was then a case of hooking my event listener in to the driver within the factory class.  This was done by wrapping the existing driver with an event firing driver, and registering the event listener against it.  This ended up something like below (using a remote chrome driver in this instance).

package common.driver;

import java.net.URL;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.events.EventFiringWebDriver;
import org.openqa.selenium.support.events.WebDriverEventListener;

public class WebDriverFactory {

	private String remoteURL;
	private WebDriver selenium;
	private WebDriverEventListener eventListener;

	public WebDriverFactory(String remoteURL) {
		this.remoteURL = remoteURL;
		this.eventListener = new MyWebDriverEventListener();
	}

	public void start() {
		try {
			selenium = new EventFiringWebDriver(new RemoteWebDriver(new URL(remoteURL), DesiredCapabilities.chrome())).register(eventListener);
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			//
		}
	}

	public void stop() {
		selenium.quit();
	}

	public WebDriver getSelenium() {
		return selenium;
	}
}

Plugging in a logging mechanism

Right, things were looking up at this point.  I could run scenarios without a hitch, but I wanted some logging output to see the fruits of my experiments.  So I gave myself a log to look with by using LogFactory from Apache Commons.  Quite simple to do by adding this single line in to the event listener class.

private Log log = LogFactory.getLog(this.getClass());

… and then adding the logger to the log4j.properties file in the Twist project.

# WebDriver event listener logger
log4j.logger.common.driver.LoggingWebDriverEventListener = DEBUG

Let the experiments begin

With the basic mechanical framework now in place, it was time to start having some fun with it.

Logging navigation

I started with something simple to prove the wiring.  An implementation of the before navigate to method.

	public void beforeNavigateTo(String url, WebDriver selenium){
		log.info("WebDriver navigating to:'"+url+"'");
	}

All this should do is log the URL being navigated to, and a high five was due because this was the output on executing an existing Twist scenario.

0    [pool-3-thread-1] INFO  common.driver.LoggingWebDriverEventListener  - WebDriver navigating to:'http://www.google.co.uk'

Now for something more useful for debugging in CI execution

The real benefit of logging to our teams is to give some extra diagnostic feedback when running scenarios in CI.  So at this point I set myself a few goals.

  • Trap driver exceptions
  • Provide useful contextual information
This was the first stab at doing that.
package common.driver;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.events.WebDriverEventListener;

public class LoggingWebDriverEventListener implements WebDriverEventListener {

	private Log log = LogFactory.getLog(this.getClass());
	private By lastFindBy;
	private String originalValue;

	public void beforeNavigateTo(String url, WebDriver selenium){
		log.info("WebDriver navigating to:'"+url+"'");
	}

	public void beforeChangeValueOf(WebElement element, WebDriver selenium){
		originalValue = element.getValue();
	}

	public void afterChangeValueOf(WebElement element, WebDriver selenium){
		log.debug("WebDriver changing value in element found "+lastFindBy+" from '"+originalValue+"' to '"+element.getValue()+"'");
	}

	public void beforeFindBy(By by, WebElement element, WebDriver selenium){
		lastFindBy = by;
	}

	public void onException(Throwable error, WebDriver selenium){
		if (error.getClass().equals(NoSuchElementException.class)){
			log.error("WebDriver error: Element not found "+lastFindBy);
		} else {
			log.error("WebDriver error:", error);
		}
	}

	public void beforeNavigateBack(WebDriver selenium){}
	public void beforeNavigateForward(WebDriver selenium){}
	public void beforeClickOn(WebElement element, WebDriver selenium){}
	public void beforeScript(String script, WebDriver selenium){}
	public void afterClickOn(WebElement element, WebDriver selenium){}
	public void afterFindBy(By by, WebElement element, WebDriver selenium){}
	public void afterNavigateBack(WebDriver selenium){}
	public void afterNavigateForward(WebDriver selenium){}
	public void afterNavigateTo(String url, WebDriver selenium){}
	public void afterScript(String script, WebDriver selenium){}

}

So with those new method implementations, I caught the exceptions and handled a specific case (not finding an element).  Also, I put some fields in to remember state for context feedback, and this gave a satisfying output like below.

0    [pool-3-thread-1] INFO  common.driver.LoggingWebDriverEventListener  - WebDriver navigating to:'http://www.google.co.uk'
1453 [pool-3-thread-1] INFO  common.driver.LoggingWebDriverEventListener  - WebDriver changing value in element found by id or name "q" from '' to 'thoughtworks twist'
1478 [pool-3-thread-1] ERROR common.driver.LoggingWebDriverEventListener  - WebDriver error: Element not found By.className: gac_c

Conclusion

I thought the spike worked out well.  There is still a whole load of useful things that could be done here, but out of spike I want to just add them in as we need them.  I’ve handled a couple of things to get a flavour, but there is no point in building a Rolls Royce when may be a push bike will do.  Let me know your thoughts on this, possibly correct my bad coding, or show me how you’ve found some other great way of extending this solution.

Next… How do I get screen shots on Twist HTML reports from a remote IPhone driver???  That should be fun!

Configuring Twist for Selenium 2

The Challenge

I have been happily using the Sahi driver with Twist for the last few months now.  However, I wanted to try driving some mobile devices in via Twist with Selenium 2 to give our team some extra scope and flexibility.  Fortunately, this proved achievable with a small amount of code and some Spring configuration with Twist.

Creating a Driver Factory

The Twist documentation provides some excellent documentation on how to switch to alternative Selenium drivers.  So I essentially followed that template.  After downloading and referencing the Selenium 2 jars in project class path, I created a factory class for WebDriver with the following code:

package twist.drivers;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.android.AndroidDriver;
import org.openqa.selenium.iphone.IPhoneDriver;

public class WebDriverFactory {
  
  private enum DeviceType {
    iphone, android
  }

  private WebDriver webDriver;
  private DeviceType deviceType;
  private String deviceURL;

  public WebDriverFactory(String deviceType, String deviceURL) {
    this.deviceType = DeviceType.valueOf(deviceType);
    this.deviceURL = deviceURL;
  }

  public void start() {
    try {
      if(deviceType.equals(DeviceType.iphone)) {
        webDriver = new IPhoneDriver(deviceURL);
      } else if(deviceType.equals(DeviceType.android)) {
        webDriver = new AndroidDriver(deviceURL);
      } else {
        throw new RuntimeException("Device type not selected");
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      //
    }
  }

  public void stop() {
    webDriver.quit();
  }

  public WebDriver getWebDriver() {
    return webDriver;
  }
}

An enumeration was used to allow the management of execution device targets.  In this case, I switched the appropriate driver implementation to the interface based on these values.  These could as easily be different browser types such as Firefox, IE, Safari, or Google Chrome.

Configuring the Spring Context (Suite)

The “applicationContext-suite.xml” file was then configured to use the driver factory with the following XML:

<bean id="webDriverFactory" class="twist.drivers.WebDriverFactory" init-method="start"
    destroy-method="stop" lazy-init="true">
  <constructor-arg value="${webdriver.device.type}"/>
  <constructor-arg value="${webdriver.device.url}"/>
</bean>

<bean id="webdriver" factory-bean="webDriverFactory" factory-method="getWebDriver" lazy-init="true" />

You’ll probably notice that both the device type and device URL are being injected in to the constructor via placeholders.  This meant I could give the flexibility of launching to different execution targets based on properties file settings (see more on this at my previous post Switch Test Runner Browsers Easily).

Scenario Writing and Test Code

One real advantage of using a tool like Twist is that the scenario writing and high level test code doesn’t need to be altered when switching or evolving drivers.  In fact, it shows that you’re along the right lines with scenario definition if that is the case.  This is especially true when you code to an automation framework pattern that abstracts site implementation along the lines of PageObjects (see the Selenium 2 wiki for further information on PageObjects).

The bottom line is that however you code your automation, you can now pick up and use the driver by passing it in via a constructor.  The example below gives a snippet of how this might look for a workflow class without such a pattern being used for a simple illustration of using the driver Spring bean:

package web.workflows;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import static org.junit.Assert.assertThat;
import static org.hamcrest.core.Is.is;

public class HomePageTests {

  private WebDriver driver;

  public HomePageTests(WebDriver driver) {
    this.driver = driver;
  }

  private WebElement dateDropDown(){
    return driver.findElement(By.id("date"));
  }

  public void setDate(String date){
    dateDropDown().sendKeys(date);
  }

  public String getDate(){
    return dateDropDown().getValue();
  }

  public void verifyDateSet(String date){
    setDate(date);
    assertThat(getDate(), is(date));
  }
}

Conclusion

The flexibility and ease at which this can be a achieved with Twist can’t be understated.  You can be up and running with Selenium 2 in a very short amount of time.  The other side of this spike for me was getting the mobile devices set up for driving tests through both real devices and simulators.  However, that’s another post in itself (especially the iPhoneWebDriver)… I’ll post that as a part II to this post at a later date.

As ever, please feel free to post comments or alternative views.  I’ll try to help where I can.