Here’s the outline: I’m working in a Java project, on a Maven-based build. I want to run a bunch of Selenium tests in a bunch of different browsers. I would like, if possible, to use the same Selenium-backed browser instance across all of my test cases, which are split across several classes. Another thing that would be sort of nice is if it didn’t try to run tests in the InternetExplorerDriver when I’m not running the tests on Windows. It turns out this is possible!
The backdrop for all this is: Maven 3.x, w/Failsafe Plugin to run Integration tests, Junit 4 as the test running framework, and Selenium 2 “Web Driver” based tests. There’s a lot more that I learned while doing this, about the cargo plugin, but that’s out of band for this particular set of issues.
The basic problem I’m trying to solve here is that if you have each test case (class) instantiate its own WebDriver instance (using, say, @BeforeClass to build it and @AfterClass to tear it down), you’ll have nicely isolated tests, but if you have a lot of those, it will really slow your test runs down. JUnit 4 doesn’t make it easy to instantiate a Suite and inject an object into each test case (at least, all the means I tried for that failed), so you need to bring your own infrastructure. Along the way, I also tried to tackle the issue of only trying to run tests on browsers you actually have available, which seems to require some fairly complex tests, and I didn’t want to try to tackle that with profiles in Maven (which was my first instinct).
Here’s what you need:
- A
WebDriverHolderclass that contains twoThreadLocals, one to hold the currentWebDriverand one to hold a boolean indicating whether the currentWebDriveris available. It might look like this:package net.clownsinmycoffee.itest; import org.openqa.selenium.WebDriver; import org.openqa.selenium.htmlunit.HtmlUnitDriver; /** * Utility class that contains the current Selenium context required by test cases that wish to share a WebDriver instance. */ public class WebDriverHolder { private static final ThreadLocal local = new ThreadLocal() { @Override public WebDriver initialValue() { return new HtmlUnitDriver(); } }; private static final ThreadLocal availableLocal = new ThreadLocal() { @Override public Boolean initialValue() { return Boolean.FALSE; } }; public static void setWebDriver(WebDriver driver) { WebDriver old = local.get(); if ( old != null ) { local.remove(); old.quit(); } local.set(driver); } public static WebDriver getWebDriver() { return local.get(); } public static void setCurrentDriverAvaialble(boolean available) { availableLocal.set(available); } public static boolean isCurrentDriverAvailable() { return availableLocal.get(); } public static void clear() { local.remove(); availableLocal.remove(); } }There’s a bit of defensive coding I’m not 100% sure I need, but I’m new to this Selenium Java game.
- An
abstractbase “BrowserSuite” class that looks like so:package net.clownsinmycoffee.itest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ TestCaseA.class, TestCaseB.class }) public abstract class AbstractBrowserSuite { }The classes referenced in the
@SuiteClassesannotation will be run by each concrete subclass of this, so you only have to edit the abstract class when you add a new test case. -
For each browser in which you want to run these tests, a concrete class named something like
ITChromeSuiteTestthatextendsAbstractBrowserSuite. Note that, by default, Failsafe will run only classes following certain naming conventions (roughly: either starting with or ending with “IT”), so by naming the “suite”s appropriately and *not* naming the individual test case classes according to that convention, we ensure that the individual ones will be run in the environment set up by the suites. Anyhow, for the “suite” classes, implement@BeforeClassand@AfterClassmethods that populate theWebDriverHolderappropriately, based on the availability of the driver in question. To wit:package net.clownsinmycoffee.itest; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.lang3.SystemUtils; import org.junit.AfterClass; import org.junit.BeforeClass; import org.openqa.selenium.chrome.ChromeDriver; import java.io.File; public class ITChromeTestSuite extends AbstractBrowserSuite { private static ChromeDriver driver; @BeforeClass public static void setupSuite() { if ( isChromeDriverAvailable() ) { driver = new ChromeDriver(); WebDriverHolder.setWebDriver(driver); WebDriverHolder.setCurrentDriverAvaialble(true); } } private static boolean isChromeDriverAvailable() { if (System.getProperty("webdriver.chrome.driver") != null) { File cdFile = new File(System.getProperty("webdriver.chrome.driver")); if (cdFile.exists() && cdFile.canExecute()) { return true; } } if (SystemUtils.IS_OS_UNIX) { CommandLine cmdLine = CommandLine.parse("which chromedriver"); try { int result = new DefaultExecutor().execute(cmdLine); return result == 0; } catch (Exception e) { throw new RuntimeException("unable to check for chromedriver", e); } } else { CommandLine cmdLine = CommandLine.parse("where /q chromedriver.exe"); try { DefaultExecutor exe = new DefaultExecutor(); return exe.execute(cmdLine) == 0; } catch (Exception e) { throw new RuntimeException("unable to check for chromedriver.exe", e); } } return false; } @AfterClass public static void tearDownSuite() { WebDriverHolder.clear(); if ( driver != null ) { driver.quit(); } } }N.B.: the
CommandLineandDefaultExecutorclasses come from Apache Commons Exec, whileSystemUtilsis from Apache Commons LangSo now what you have is, a master list of tests you want to run in each browser, and a way of dynamically detecting whether any of those tests should be run. Here’s where it gets a bit disappointing, although there is a solution; you might think you could use JUnit4′s
Assumeframework in either the@BeforeClassmethod or in a@Beforemethod on the*Suiteclasses. Alas, you cannot; if you try the former, theSuitewill error, and the latter seems to be ignored. - The solution to the above is to put the
Assumecheck on a@Beforemethod in an abstract class that all your concrete test cases inherit from. This means that, for each@Testin your class, the assumption will be checked and the test will be ignored, which is not ideal, but it works. It might look a little like this:package net.clownsinmycoffee.itest; import com.thoughtworks.selenium.Selenium; import org.junit.Assume; import org.junit.Before; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverBackedSelenium; import java.net.URI; import static org.junit.Assert.*; /** * */ public abstract class BaseWebDriverTest { protected URI siteBase = URI.create("http://localhost:8181"); private WebDriver webDriver; protected Selenium selenium; @Before public void setUp() { Assume.assumeTrue("Current driver is not available", WebDriverHolder.isCurrentDriverAvailable()); } @Override public void setWebDriver(WebDriver webDriver) { this.webDriver = webDriver; } @Override public WebDriver getWebDriver() { if ( webDriver == null ) { this.webDriver = WebDriverHolder.getWebDriver(); } return webDriver; } protected Selenium getNewSelenium() { return new WebDriverBackedSelenium(getWebDriver(), siteBase.toString()); } protected Selenium getNewSelenium(String path) { return new WebDriverBackedSelenium(getWebDriver(), getPageUrl(path)); } public String getPageUrl(String path) { return siteBase.resolve(path).toString(); } }While I was at it, I added a bunch of other utility methods needed across my tests.
Hope this helps somebody out there!
