Single WebDriver Instance in Maven

Quote

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:

  1. A WebDriverHolder class that contains two ThreadLocals, one to hold the current WebDriver and one to hold a boolean indicating whether the current WebDriver is 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.

  2. An abstract base “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 @SuiteClasses annotation 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.

  3. For each browser in which you want to run these tests, a concrete class named something like ITChromeSuiteTest that extends AbstractBrowserSuite. 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 @BeforeClass and @AfterClass methods that populate the WebDriverHolder appropriately, 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 CommandLine and DefaultExecutor classes come from Apache Commons Exec, while SystemUtils is from Apache Commons Lang

    So 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 Assume framework in either the @BeforeClass method or in a @Before method on the *Suite classes. Alas, you cannot; if you try the former, the Suite will error, and the latter seems to be ignored.

  4. The solution to the above is to put the Assume check on a @Before method in an abstract class that all your concrete test cases inherit from. This means that, for each @Test in 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!

UnMac’d

So, one of my first posts on this here blog, long ago, detailed the fact that I’d bought a Mac. Well, for various reasons, not least of which are their increasingly nasty corporate legal direction and the fact that basically, the goal of every Apple product’s design these days seems to be to herd you into the walled garden where they make all the major decisions for you (and that includes you, fellow software folks), I don’t use one at home any more. I have recommended the products to people in the past, but no more. I still use one at work, but not particularly by choice.

Anyhow, in that earlier post I included a screenshot (which got lost in a file shuffle some time back), so for this mirror image post I thought I’d do another one. Perhaps it will illustrate my discontent further.

So, that's why I should upgrade my OS, eh?

Seriously, that’s what’s important.  Or, to be fair, that’s what they’re betting is important to me.  And that right there folks, is the Most Valuable Company in the world right now.

Sigh.

In Which I Dabble in Schadenfreude

If the reports are true, Oracle has really dropped the ball with security in client-side Java. After the recent fanfare (OK, in some circles) around the availability of the client-side JavaFX on all platforms supported by the JDK, the next bit of news suggests they’ve been sitting on massive, horrible, holy-smokes-your-hair-should-be-on-fire security vulnerabilities for four months, nigh on two major releases, and that they don’t plan to fix the (long chain of adjectives expressing what a big problem it is) problem until October? Meanwhile IT departments, at least the ones who support Java 7 on the client, are having to run around coming up with a strategy for disabling the Java plugin (the vulnerability was apparently introduced in Java 7 and does not exist in Java 6). And there has been, as I write this two days after the revelation the exploit is being actively … exploited … nothing, not even “hang on, we’re gonna see what we can do about it” from Oracle[1]. Bad form, Oracle.

Update: JDK 7u7 is now available, and appears to fix the problem; released the day after this post was made. Kudos for getting the fix out, but it[2] is tempered by the delay in reacting to the issue when it was initially reported and the silence leading up to the release. Oracle still apparently has a lot to learn about handling software for consumers, assuming its heart is in that game.

I think it comes down to this: this affair underscores a long-standing tradition — whatever Oracle may be good at, it ain’t the client side generally, and in particular, it ain’t the whole consumer side of things. Since back when Java was a Sun thing, the client was never really quite “there.” The visuals and startup time, and in general deployment scenario for applets made them hard to implement well, and even when that happened you couldn’t call the end-user’s reaction to the result “delight.” Its replacement, Web Start, to be fair, was a pretty good idea that didn’t address the first two issues and didn’t get far enough with the third. It’s gotten to the point where I’m not really sure whether the situation has improved (thanks in large part to Javascript and DOM near-standardization).

Oracle’s tradition as a non-entity on the client side is one of the things that was amusing (again, in some circles) about their lawsuit against Google and Android — Sun wasn’t exactly brilliant at delivering things non-developers actually like to use, and Oracle isn’t particularly good (outside of a pretty limited circle, in my experience) at delivering things developers like to use. So, when Oracle launched its lawsuit alleging massive damages, I was thinking that it was kind of funny since Oracle never, in seventeen years, would have been able to pull off a tenth of the same thing with that technology (this is neither here nor there when it comes to the merits of the suit from a legal standpoint — but see the post title, and at any rate — it looks like those alleged merits weren’t “there” in the first place, given the outcome of the trial).

But then, Oracle products are not exactly something consumers buy, are they? This suggests to me that Oracle’s organizational culture makes it hard for the corporate them to occupy the right standpoint to see how big a problem this sort of thing is. I have a long-standing thesis that pertains to “enterprise” software and its nearly universally acknowledged suckage: the people that buy it are not, as a rule, the people that have to use it. So the suckage, at best, takes a couple of C*O generations at least to have an impact on repeat purchases, as there’s a long time between the time when someone moves from being a peon in the information mines to the level of institutional authority where they can have a serious impact on purchasing decision. Thus there’s pretty much no incentive for the enterprise vendors to work on usability; and apparently, when the paradigm tries to do this … zero-day-minus-four-months vulnerabilities with a distinct lack of communication are the result. Oh and, lest I be accused of being too reductionist in this diagnosis, let me state for the record that enterprise problems are also often very hard and solving them even 60% of the way leaves precious little time for implementing eye-pleasing infinite scrolling javascript widgets.

[1] Maybe it takes longer than I thought for information to get to Lanai and back. Zing! I bet that stings, Mr. Ellison! It will probably take a lot of macadamias to soften that blow!

[2] Yeah, I’m pretty sure that word is singular. Must be something to do with my heritage.

w3c.recommend(xproc)

As an unabashed fan of the angle brackety type things, I’m chuffed to learn, via Norman Walsh,  that XProc is now a W3C recommendation. Congratulations to all the people who put in all the work to get it there.  Take a look at if if you need to run your XML documents through a bunch of steps and produce a bunch of results (and do other things along the way).

I’ve used XProc in a limited way to run a sort of enhanced XSLT process, and it was slow to get started, but once I wrapped my head around the central concepts, the rest went like butter.  Given that the specification provides for making HTTP requests, I’d think it could serve as an especially useful component in a RESTful document publishing architecture.  But then, I would say that.

On Bankers

Here’s some unsolicited advice for performing long-distance one-off transactions with financial institutions: if your transaction is at all unusual and requires that documentation of some sort or another be passed around and notarized and suchlike — get the procedure documented.  That person  you’re talking to on the phone, unless you’re really lucky, doesn’t know that the dark, cold hands of Institutional Policy are poised to strangle any vestiges of trust you have in corporate behavior if you follow the seemingly simple procedure.  The bureaucracy always wants a Very Serious Document prepared, and it’s quite easy to blow you off, because what are you but some disembodied, highly compressed voice coming out over a Very Small Speaker?

Of course, if banks weren’t somewhat risk averse, we wouldn’t put our money into them.  Know, however, that the risk aversion is about you (the customer) not so much or not at all, and almost entirely about the Organization.  And the rules and regulations governing this stuff are Very Complex indeed, and typically aren’t the sorts of things that are known by the type of employee who has to (eurk!) take phone calls from the public. If they knew that, they’d be too busy in meetings deciding what the next set of rules are to take the time to deal with your piddly little problems.

The beauty of this whole setup is that it takes no active or deliberate malice on any individual’s part, and you, dear friend, will have to seek and pay for the advice of a lawyer.  Ain’t modern life grand?  Philly Joe Remarkable is not the only one looking on in disbelief.

On Nostalgia

There’s a whiff of wistfulness out there on the ‘tubes for the passing of Sun Microsystems, and I’ve got to admit I’ve participated a bit in that; the absorption by Oracle of Sun’s assets certainly marks some kind of transition in the industry that helps me pay my bills, call it ‘maturity’ or ‘loss of innocence’, or “oh no, we’re all doomed!”

On the other hand, I was clearing out some gunk in my attic this evening, and I came across a pretty hefty printout that details how to write a very simple custom component for Java Server Faces 1.0; it clocks in at around 15 pages or so. And then, you know, maybe I’m not so surprised at what happened to Sun.

And then it hits me that what’s swallowing Sun is Oracle. And then I’m surprised again.

File under “Vendorprisey”

The Third G Drops

I’ve been thinking of it as effectively a rumour up until now, but today, my Android phone started getting a 3G signal in Chapel Hill and Carrboro (that’s T-Mobile, in case you didn’t know). So, now I go from having been a double early-adopter sucker (64Kbits/s and the first generation phone) to a reasonably fast-browsin’ (750-880Kbits/s) early-adopter sucker.

It’s progress, I guess. I understand that it’s possible to write applications for these things. If only I knew how to use a computer.

In Which I Become a Food Blogger

Some time ago, a friend pointed me to this magical stuff that turns fats into powders. T’other day, I finally got my hands on some of this tapioca maltodextrin, as it’s called; it’s a starch, and there’s really not much more to turning a really fatty thing into a powder than mixing the two things together.

The starch is close enough to flavourless, but any statements you may have encountered to the effect that you put the powdered (olive oil/peanut butter/hazelnut-and-chocolate-spread) into your mouth and voilà! it’s the original stuff again! are not really operative. There’s a noticeable effect on the texture, and you’ve got a bunch of starch that wasn’t there before.

Still, it’s amazing to work with and it’s truly a surprise for your taste buds.

Generating CSV from XML

I was helping a friend out recently who wanted to import some XML data he got into a more useful format [ ed. WHAT? err, useful to him, 'kay?].  It seems like there are a few services out there that will give you data in some kind of home-grown XML format in a record-oriented structure, e.g.

<contacts>
    <contact>
        <id>...</id>
        <name>....</name>
        <email>...</email
    </contact>
    ... <!-- more contact elements -->
</contacts>

When you have data like this, what you’ve got is essentially a degenerate spreadsheet, easily represented as CSV.  But if the service doesn’t provide CSV export, you can get it fairly easily via XSLT.  The idea is, you want to output one row (the header) with the names of the elements in each record, and then output each row thereafter.  What matters, as far as the input, is that it has the structure mentioned above: the document consists of a root element with a number of child elements, each one of which represents a record in the data. Note that the following restriction applies: each record element must contain the same number of child elements in the same order. In order to make it a little more robust, I added some logic to quote non-numeric values, which should provide a reasonable amount of protection from values that contain commas.   For extra fun (and this was my friend’s idea, and I was too lazy to follow through the steps) you could register this XSLT as a filter in OpenOffice.org so you can (nearly) automatically import these files into oocalc. It’s not entirely elegant (the logic for outputting the header row is duplicated with the logic for outputting a normal row), but it gets the job done. So here it is, I place it in the public domain.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="text"
    encoding="iso-8859-1"/>
    
    <xsl:template match="/">
        <xsl:variable name="records" select="*/*"/>
        <xsl:call-template name="header-row">
            <xsl:with-param name="header" select="$records[1]"/>
        </xsl:call-template>
        <xsl:for-each select="*/*">
            <xsl:call-template name="output-row"/>
        </xsl:for-each>
    </xsl:template>
    
    <xsl:template name="output-row">
        <xsl:for-each select="child::*">
            <xsl:variable name="numeric" select="not(string(number(.)) = 'NaN')"/>
            <xsl:choose>
                <xsl:when test="$numeric">
                    <xsl:value-of select="normalize-space(.)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:text>"</xsl:text>
                    <xsl:value-of select="normalize-space(.)"/>
                    <xsl:text>"</xsl:text>
                </xsl:otherwise>
            </xsl:choose>
            
        <xsl:choose>
            <xsl:when test="position() = last()">
                <xsl:text>&#13;&#10;</xsl:text>
            </xsl:when>
            <xsl:otherwise>
            <xsl:text>,</xsl:text>
            </xsl:otherwise>
        </xsl:choose>
        </xsl:for-each>
    </xsl:template>
    
    <xsl:template name="header-row">
        <xsl:param name="header"/>
        <xsl:for-each select="$header/*">
            <xsl:call-template name="quotevalue"/>
        </xsl:for-each>
    </xsl:template>
    
    <xsl:template name="quotevalue">
        <xsl:text>"</xsl:text>
        <xsl:value-of select="normalize-space(name(.))"/>
        <xsl:text>"</xsl:text>
        <xsl:choose>
            <xsl:when test="position() != last()">
                <xsl:text>,</xsl:text>
            </xsl:when>
            <xsl:otherwise>
                <xsl:text>&#13;&#10;</xsl:text>
            </xsl:otherwise>
        </xsl:choose>        
    </xsl:template>    
</xsl:stylesheet>