Idiom for Browser-Selectable Selenium Tests

For some time I’ve wanted to share an idiom I personally use and recommend when building Selenium Tests. This idiom allows to control which browsers are used to run the tests without needing to update test sources or configuration.

The simple ideas behind this idiom are:

  • Test code and configuration should not depend on the test environment.
  • Tests can be executed in any given browser, independently from others.
  • To change the browsers used for test execution, it is not needed to update test sources or configuration.
  • Selenium Grid URL and application URL are also configurable.
  • Both environment variables and Java system variables can be used.
  • All settings have sensible defaults.

I call this idiom ‘Browser-Selectable Tests’. I promise I keep thinking on a better name :-)

Step 1. Writing the test case.

The first step is to write a test case. Alternatively you may use Selenium IDE to record user interactions. Anyway, you will end with a re-executable test case that will interact with an existing web application, its pages, forms and widgets.

To be independent from the test environment, the test method should receive two parameters: the browser driver and the base URL of the target application. Rest of the test method is as you would expect it to be:

public void testMyFunctionality(final WebDriver driver, final String baseUrl) {
    try {
        // wait for the application to get fully loaded
        WebElement someLink = (new WebDriverWait(driver, 5)).until(
                new ExpectedCondition<WebElement>() {
            public WebElement apply(WebDriver d) {
                d.get(baseUrl);
                return d.findElement(By.linkText("some text"));
            }
        });
        // do something with the page, add assertions, verify outputs
        ...
    } finally {
        if (driver != null) {
            driver.quit();
        }
    }
}

Although it may seem obvious it is key to write tests this way, as the same method can be invoked to run tests in different browsers.

Step 2. Obtaining configuration from environment or system.

Next step is to read configuration from environment variables or system properties and add the code needed to invoke the test case depending on selected browsers.

We are going to read String and boolean properties, and we decide that Java system properties takes precedence from environment variables. Let’s build a method to read configuration values:

private static String getConfigurationProperty(
        String envKey, String sysKey, String defValue) {
    String retValue = defValue;
    String envValue = System.getenv(envKey);
    String sysValue = System.getProperty(sysKey);
    // system property prevails over environment variable
    if (sysValue != null) {
        retValue = sysValue;
    } else if (envValue != null) {
        retValue = envValue;
    }
    return retValue;
}
private static boolean getConfigurationProperty(
        String envKey, String sysKey, boolean defValue) {
    boolean retValue = defValue;
    String envValue = System.getenv(envKey);
    String sysValue = System.getProperty(sysKey);
    // system property prevails over environment variable
    if (sysValue != null) {
        retValue = Boolean.parseBoolean(sysValue);
    } else if (envValue != null) {
        retValue = Boolean.parseBoolean(envValue);
    }
    return retValue;
}

Using these methods, we can read as much settings as needed to configure the test environment:

private static final Logger logger = LoggerFactory.getLogger(MyTestCase.class);
private static boolean RUN_HTMLUNIT;
private static boolean RUN_IE;
private static boolean RUN_FIREFOX;
private static boolean RUN_CHROME;
private static boolean RUN_OPERA;
private static boolean RUN_ANDROID;
private static String SELENIUM_HUB_URL;
private static String SELENIUM_HUB_URL_ANDROID;
private static String TARGET_SERVER_URL;
private static String TARGET_SERVER_URL_ANDROID;

@BeforeClass
public static void initEnvironment() {
    RUN_HTMLUNIT = getConfigurationProperty("RUN_HTMLUNIT", "test.run.htmlunit", true);
    logger.info("running the tests in HtmlUnit: " + RUN_HTMLUNIT);

    RUN_IE = getConfigurationProperty("RUN_IE", "test.run.ie", false);
    logger.info("running the tests in Internet Explorer: " + RUN_IE);

    RUN_FIREFOX = getConfigurationProperty("RUN_FIREFOX", "test.run.firefox", false);
    logger.info("running the tests in Firefox: " + RUN_FIREFOX);

    RUN_CHROME = getConfigurationProperty("RUN_CHROME", "test.run.chrome", false);
    logger.info("running the tests in Chrome: " + RUN_CHROME);

    RUN_OPERA = getConfigurationProperty("RUN_OPERA", "test.run.opera", false);
    logger.info("running the tests in Opera: " + RUN_OPERA);

    RUN_ANDROID = getConfigurationProperty("RUN_ANDROID", test.run.android", false);
    logger.info("running the tests in Android: " + RUN_ANDROID);

    SELENIUM_HUB_URL = getConfigurationProperty("SELENIUM_HUB_URL",
        "test.selenium.hub.url", "http://localhost:4444/wd/hub");
    logger.info("using Selenium hub at: " + SELENIUM_HUB_URL);

    SELENIUM_HUB_URL_ANDROID = getConfigurationProperty("SELENIUM_HUB_URL_ANDROID",
        "test.selenium.hub.url.android", "http://localhost:4448/wd/hub");
    logger.info("using Selenium hub for Android at: " + SELENIUM_HUB_URL_ANDROID);

    TARGET_SERVER_URL = getConfigurationProperty("TARGET_SERVER_URL",
        "test.target.server.url", "http://localhost:8180/petclinic");
    logger.info("using target server at: " + TARGET_SERVER_URL);

    TARGET_SERVER_URL_ANDROID = getConfigurationProperty("TARGET_SERVER_URL_ANDROID",
        "test.target.server.url.android", "http://localhost:8180/petclinic");
    logger.info("using target server for Android at: " + TARGET_SERVER_URL_ANDROID);
}

Step 3. Invoking or skipping a test depending on the test environment.

Next step is to build the actual test methods that will invoke the test case or skip it depending on the test environment variables/properties just read:

@Test
public void testHtmlUnit() throws MalformedURLException, IOException {
    Assume.assumeTrue(RUN_HTMLUNIT);
    WebDriver driver = new HtmlUnitDriver();
    testMyFunctionality(driver, TARGET_SERVER_URL);
}

@Test
public void testIE() throws MalformedURLException, IOException {
    Assume.assumeTrue(RUN_IE);
    DesiredCapabilities browser = DesiredCapabilities.internetExplorer();
    WebDriver driver = new RemoteWebDriver(new URL(SELENIUM_HUB_URL), browser);
    testMyFunctionality(driver, TARGET_SERVER_URL);
}

@Test
public void testFirefox() throws MalformedURLException, IOException {
    Assume.assumeTrue(RUN_FIREFOX);
    DesiredCapabilities browser = DesiredCapabilities.firefox();
    WebDriver driver = new RemoteWebDriver(new URL(SELENIUM_HUB_URL), browser);
    testMyFunctionality(driver, TARGET_SERVER_URL);
}

@Test
public void testChrome() throws MalformedURLException, IOException {
    Assume.assumeTrue(RUN_CHROME);
    DesiredCapabilities browser = DesiredCapabilities.chrome();
    WebDriver driver = new RemoteWebDriver(new URL(SELENIUM_HUB_URL), browser);
    testMyFunctionality(driver, TARGET_SERVER_URL);
}

@Test
public void testOpera() throws MalformedURLException, IOException {
    Assume.assumeTrue(RUN_OPERA);
    DesiredCapabilities browser = DesiredCapabilities.opera();
    WebDriver driver = new RemoteWebDriver(new URL(SELENIUM_HUB_URL), browser);
    testMyFunctionality(driver, TARGET_SERVER_URL);
}

@Test
public void testAndroid() throws MalformedURLException, IOException {
    Assume.assumeTrue(RUN_ANDROID);
    WebDriver driver = new AndroidDriver(new URL(SELENIUM_HUB_URL_ANDROID));
    testMyFunctionality(driver, TARGET_SERVER_URL_ANDROID);
}

Real magic is done by JUnit’s Assume class. If the boolean condition is false the test method is skipped, just as we want it to be. If true, the corresponding WebDriver instance is prepared and sent to the test case.

Step 4. Executing the tests.

Once ready following previous steps, the test case can be directly executed without any configuration parameter needed. Sensible defaults imply that the test case will be executed with HtmlUnit’s in-memory browser and skipped for the rest.

// execute test only with HtmlUnit (no Selenium Grid is required)
mvn test

If we want to activate some of the browsers, we just need to provide the system properties or to set up the environment variables.

If executing the tests via Maven (command-line or continuous integration), don’t forget to wrap up system properties using argLine system property, like this:

// execute test with HtmlUnit, Internet Explorer and Chrome (using default Selenium Grid @ localhost)
mvn test -DargLine="-Dtest.run.htmlunit=true -Dtest.run.ie=true -Dtest.run.firefox=false -Dtest.run.chrome=true -Dtest.run.opera=false -Dtest.run.android=false"

Step 5. Base abstract test case.

It may seem obvious, but anyway take into consideration that above idiom can be easily encapsulated into a base abstract test case. Only the test method (the one receiving the browser and URL as parameters) would be defined abstract and implemented in derived test classes.

Conclusion.

I hope this is useful for you and helps to spread adoption of automated tests. Never forget that manual tests are boring!

About these ads

2 thoughts on “Idiom for Browser-Selectable Selenium Tests”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s