BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Headless Selenium Browsers

Headless Selenium Browsers

Leia em Português

Key Takeaways

  • Headless browsers run without a UI.
  • PhantomJS is no longer supported.
  • JBrowser Driver is a low overhead option for Java 8.
  • All current Java headless selenium drivers require installing a real browser if you need Java 11 support.
     

Selenium is a well-known, powerful tool for automated testing in web browsers. While Selenium Web driver supports all the major browsers, you don’t always want the costs of testing in a real browser. Headless browsers to the rescue! The examples in this article are in a GitHub repo. All run with JUnit 5 and Maven. Java 11 vs Java 8 support is noted for each example.

What are the benefits of a headless browser?

Headless browsers run without a user interface (UI.) A big benefit of using one for testing is performance - since headless browsers don’t have a UI, they are faster than real browsers.

There’s another advantage of some headless browsers – dependencies. When testing on a continuous integration server like Jenkins, the machine might not have a real browser installed. And depending on your environment, you might not have permissions to install one.

One the other hand headless browsers that require a “real” browser to be installed are great for development. For example, both Chrome and Firefox have the option to run in headless mode. It is really helpful when debugging a Selenium script to temporarily turn off headless mode and watch the program run. That way you can visually see where things go haywire.

HtmlUnitDriver – the initial headless driver

In the past, Selenium came with a built in headless driver called HtmlUnitDriver. While this driver is still supported, it is now a separate dependency and, unsurprisingly, uses the Html Unit framework. Prior to Single Page Applications and largely AJAX based pages, this driver was an excellent choice. You have the ability to choose whether to run the page JavaScript, it runs in memory and is very fast.  It’s still a good choice for web pages with a good amount of HTML data on them.

The following code shows how to run a basic test that uses Selenium with HtmlUnitDriver. It works because InfoQ’s home page is designed to be functional without JavaScript. This example is available in both Java 8 and Java 11 versions in the GitHub repo.

package com.infoq.selenium;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;

import java.util.Set;
import java.util.stream.Collectors;

public class HtmlUnitSeleniumIT {

    protected WebDriver driver;

    // ----------------------------------------------------

    @BeforeEach
    public final void connect() {
        driver = new HtmlUnitDriver();
        //driver.setJavascriptEnabled(true);
    }

    @AfterEach
    public final void closeDriver() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void qconDates() {
        driver.get(“https://www.infoq.com“);

        Set<String> newYorkCity = driver.findElements(By.className(“qcon”))
                .stream()
                .map(element -> element.getAttribute(“innerText”))
                .filter(city -> city.trim().startsWith(“New York”))
                .collect(Collectors.toSet());
        assertEquals(1, newYorkCity.size(), “New York is an upcoming city”);

    }
}

However, uncommenting the line that enables JavaScript is a different story. After spitting out a whole pile of JavaScript warnings, it fails with EcmaError: TypeError: Cannot call method "then" of undefined.

Since many pages won’t load at all without JavaScript, this shows the need for a headless driver with better JavaScript support.

PhantomJS

For many years, PhantomJS was a great choice. It was lightweight, headless and had excellent JavaScript support. However, in April 2017, the maintainer stepped down and in March 2018, the project was formally abandoned. I miss it.

Reading the announcement and comments, the intent was clearly to move to Chrome Driver. I don’t recommend using PhantomJS for anything new. Aside from not being supported, I switched two of my projects over to Chrome Driver because PhantomJS doesn’t handle JavaScript well enough for the most current JavaScript libraries. Since Chrome Driver uses a real browser, that’s not a problem for it.

Chrome Driver

Chrome provides a headless mode, which works well overall. The biggest downside is that you need to be able to install Chrome. You don’t need a UI, but installing software is not always possible.

Chrome Driver also requires an executable to be downloaded. I cheated a bit here. I keep the executable in the same directory as the project (or in a binary repository and copy it to the workspace.) I then have the Java test itself set the permissions.  Similarly, I have the Java test set the system property within the Java process to that location. A bit of a cheat, I know, but it does keep almost everything self contained. “Almost” because it still requires Chrome itself to be installed. This works great on my personal projects. When sharing code with others though it falls apart because they also need to download the executable.

There are both Java 8 and Java 11 versions in the repo. Both require you to download the executable and replace it in the chrome-driver directory.

package com.infoq.selenium;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

import static org.junit.jupiter.api.Assertions.*;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
import java.util.stream.Collectors;

public class ChromeSeleniumIT {

    private static final boolean HEADLESS = true;
    private static final String CHROME_DRIVER_DIRECTORY = “chrome-driver”;

    protected WebDriver driver;

    // ----------------------------------------------------

    @BeforeEach
    public final void connect() {
        Path chrome = Paths.get(CHROME_DRIVER_DIRECTORY + “/chromedriver”);
        chrome.toFile().setExecutable(true);
        System.setProperty(“webdriver.chrome.driver”, chrome.toAbsolutePath().toString());

        ChromeOptions chromeOptions = new ChromeOptions();
        if (HEADLESS) {
            chromeOptions.addArguments(“--headless”);
        }

        driver = new ChromeDriver(chromeOptions);

        // https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/27
        ((JavascriptExecutor) driver).executeScript(“window.alert = function(msg) { }“);
        ((JavascriptExecutor) driver).executeScript(“window.confirm = function(msg) { }“);
    }

    @AfterEach
    public final void closeDriver() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void qconDates() {
        driver.get(“https://www.infoq.com”);

        Set<String> newYorkCity = driver.findElements(By.className(“qcon”))
                .stream()
                .map(element -> element.getAttribute(“innerText”))
                .filter(city -> city.trim().startsWith(“New York”))
                .collect(Collectors.toSet());
        assertEquals(1, newYorkCity.size(), “New York is an upcoming city”);
    }
}

The two execute script lines are to work around a problem with prompts in another application I am testing. I’ve instructed the driver to ignore them. (Although it’s not needed here, it was enough of a pain to figure out that I use it everywhere I use Chrome Driver so I never have to solve that problem!)

Another disadvantage of using Chrome Driver is that it needs to be updated periodically to support later versions of Chrome.

Gecko Driver

Chrome was first to the party of headless browser testing, and so that is the one I have the most experience with. However Firefox also has a headless mode. which works just like the Chrome one. You download the Gecko Driver and use selenium-firefox-driver in your pom.xml

JBrowser Driver

While I like Chrome Driver, it does require Chrome to be installed. I have a project where I run a daily check to tell me when Oracle changes the certification objectives. The server I run it on does not have Chrome installed. Recently, Oracle updated their website to be more AJAX heavy. PhantomJS no longer met my needs so I started looking for a more modern driver.

I found JBrowser Driver. There have been regular commits in the last year including updates to Selenium versions. It has a good license (Apache 2.) Version 1.0.0 just came out over the summer. However there have been pre-1.0 releases for almost three years. 

The biggest downside to JBrowser Driver is that they only support Oracle JDK Java 8 at present. This is the version of Java that will no longer have patches for business users in January 2019 and for individual users in December 2020.

Note on Java FX:

  • In Java 7, Java FX was a separate download
  • In Java 8, Java FX was part of Oracle JDK but not Open JDK
  • In Java 11, Java FX is available for free via a Maven dependency using OpenJFX
  • Also, in Java 11, the Robot.java class changed packages from com.sun.glass.ui to javafx.scene.robot. This means that you can’t just use OpenJFX’s version of JavaFX with your Open JDK 11 and expect JBrowser Driver to work.

The code is straightforward and the Java 8 version is in the GitHub repo.

package com.infoq.selenium;

import com.machinepublishers.jbrowserdriver.JBrowserDriver;
import com.machinepublishers.jbrowserdriver.Settings;
import com.machinepublishers.jbrowserdriver.Timezone;
import com.machinepublishers.jbrowserdriver.UserAgent;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class JBrowserSeleniumIT {

    protected WebDriver driver;

    // ----------------------------------------------------

    @BeforeEach
    public final void connect() {
        driver = new JBrowserDriver(Settings.builder()
                .timezone(Timezone.AMERICA_NEWYORK)
                .userAgent(UserAgent.CHROME).build());

        // says 120 but is really 0
        driver.manage().timeouts().pageLoadTimeout(120, TimeUnit.SECONDS);
    }

    @AfterEach
    public final void closeDriver() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void qconDates() {
        driver.get(“https://www.infoq.com“);

        Set<String> newYorkCity = driver.findElements(By.className(“qcon”))
                .stream()
                .map(element -> element.getAttribute(“innerText”))
                .filter(city -> city.trim().startsWith(“New York”))
                .collect(Collectors.toSet());
        assertEquals(1, newYorkCity.size(), “New York is an upcoming city”);
    }

}

Conclusion – comparing the options

There’s lots of choice for headless browser testing with Java and Selenium. And like any good engineering problem, there are tradeoffs to choose between. This table lists your main choices. The lesson is that if you need to be able to run a headless Selenium driver without a real browser installed, you need to stay on Java 8 for the time being.

Library

JavaScript power

Supports Java 8

Supports Java 11

Requires browser installation

Html Unit Driver

Low

Yes

Yes

No

Phantom JS (no longer supported)

Medium

Yes

No

No

Chrome Driver

High

Yes

Yes

Yes – Chrome

Gecko Driver

High

Yes

Yes

Yes – Firefox

JBrowser Driver

High

Yes

No

No

About the Author

Jeanne Boyarsky is a Java developer and part time ScrumMaster. She co-authored Wiley’s OCA/OCP 8 certification books and will be updating them for the next version of the certification. In addition to volunteering at CodeRanch, she mentors the programmers on a high school robotics team and won a mentorship award. Jeanne has spoken at conferences including JavaOne, QCon, DevNexus and SpringOne.

Rate this Article

Adoption
Style

BT