Browser Drivers — ChromeDriver, GeckoDriver, WebDriverManager

8 min read

You typed WebDriverManager.chromedriver().setup() in the previous lesson and a Chrome window opened. Behind that line is the single most-asked Selenium support question in history: "Why is my test failing with SessionNotCreatedException: Could not start a new session?" The answer, almost always, is a mismatch between the browser version and the driver binary. This lesson explains the moving parts, walks through the three eras of driver management (manual → WebDriverManager → Selenium Manager), and shows the browser options you'll add for CI.

Why drivers exist at all

Selenium doesn't talk to browsers directly. It talks to a small companion executable per browser:

  • Chrome / Chromium-basedchromedriver
  • Firefoxgeckodriver
  • Edgemsedgedriver
  • Safari (macOS only) → safaridriver (ships with the OS)

Each driver implements the W3C WebDriver protocol on one end (HTTP requests from your Java code) and the browser's native automation interface on the other. Because the browser's automation surface changes between versions, the driver binary is version-locked to the browser. Chrome 131 needs a ChromeDriver 131. Pair Chrome 131 with ChromeDriver 130 and the session refuses to start.

The three eras of driver management

Driver management — old, current, new

Manual (don't)

  • Check chrome://version

    Note the major version

  • Download matching ChromeDriver

    From chromedriver.chromium.org

  • Put it on PATH or set System.setProperty

    "webdriver.chrome.driver"

  • Chrome auto-updates → driver breaks

    Repeat the dance, every month

  • Maintenance hell

WebDriverManager (industry standard)

  • WebDriverManager.chromedriver().setup()

  • Inspects installed browser version

  • Downloads matching driver to ~/.cache

  • Reuses cache across runs — fast on CI

  • Works for Chrome, Firefox, Edge, Opera, IE

Selenium Manager (built-in, 4.6+)

  • new ChromeDriver() — that's it

  • Bundled with Selenium itself

  • No extra dependency

  • Newer, less battle-tested than WDM

  • Same idea, no extra library

For the rest of this course we'll use WebDriverManager. It's the most established, has the most options (caching, custom URLs, proxies), and is the de facto choice in Java QA shops in 2026. If you're starting greenfield and don't need the extras, Selenium Manager is a one-line simplification — feel free to swap.

WebDriverManager for each browser

WebDriverManager has a method per browser. The setup line is the only thing that changes:

import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.edge.EdgeDriver;
 
// Chrome
WebDriverManager.chromedriver().setup();
WebDriver driver = new ChromeDriver();
 
// Firefox
WebDriverManager.firefoxdriver().setup();
WebDriver driver = new FirefoxDriver();
 
// Edge
WebDriverManager.edgedriver().setup();
WebDriver driver = new EdgeDriver();

Notice the field type is always WebDriver (the interface), never ChromeDriver (the implementation). That single decision is what lets a parameterised test run the same body across all three browsers (chapter 7).

Selenium Manager — the no-dependency alternative

If you're on Selenium 4.6 or newer (you are — we declared 4.21.0 in pom.xml), the bundled Selenium Manager removes WebDriverManager entirely:

WebDriver driver = new ChromeDriver(); // that's the whole setup

When the constructor runs, Selenium Manager checks for a cached driver, downloads one if missing, and starts the browser. It works. It's simpler. It's also younger — first shipped in Selenium 4.6 (late 2022), so production teams have less collective experience with it. We mention it so you recognise it in modern tutorials; the rest of this course sticks with WebDriverManager.

Browser options — the second-most-important class

Once the driver starts, you control its behaviour with an Options object:

import org.openqa.selenium.chrome.ChromeOptions;
 
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");           // no visible browser window
options.addArguments("--window-size=1920,1080");  // virtual viewport size
options.addArguments("--disable-notifications");   // never show "Allow notifications?" popups
options.addArguments("--disable-gpu");             // older CI quirk; harmless on modern systems
options.addArguments("--no-sandbox");              // only on Linux containers (e.g., Docker)
options.addArguments("--disable-dev-shm-usage");   // workaround for /dev/shm running out in Docker
 
WebDriver driver = new ChromeDriver(options);

The arguments come from Chrome itself — the chromium command-line switches list is the canonical reference. The same idea works for Firefox:

import org.openqa.selenium.firefox.FirefoxOptions;
 
FirefoxOptions options = new FirefoxOptions();
options.addArguments("-headless");
WebDriver driver = new FirefoxDriver(options);

Headless — the flag that earns its place in CI

CI runners typically don't have a display. Trying to start Chrome in normal mode on a headless Linux runner produces:

SessionNotCreatedException: ... unknown error: Chrome failed to start: exited abnormally

Add --headless=new (the modern headless mode introduced in Chrome 109) and the browser runs without a window. Tests run identically; you just don't see anything happen. Locally, you'll usually leave headless off during development (it's nice to watch tests run while debugging), and turn it on for CI by reading an environment variable:

@BeforeMethod
public void setup() {
    WebDriverManager.chromedriver().setup();
    ChromeOptions options = new ChromeOptions();
    if ("true".equals(System.getenv("CI"))) {
        options.addArguments("--headless=new");
        options.addArguments("--no-sandbox");
        options.addArguments("--disable-dev-shm-usage");
    }
    options.addArguments("--window-size=1920,1080");
    driver = new ChromeDriver(options);
}

GitHub Actions, GitLab CI, Jenkins (when run via Docker), and most cloud CI providers set CI=true automatically. That's the lever you flip on.

A complete browser-options test

Putting it together in a runnable shape:

package com.mycompany.tests.tests;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
 
public class HeadlessChromeTest {
 
    WebDriver driver;
 
    @BeforeMethod
    public void setup() {
        WebDriverManager.chromedriver().setup();
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless=new");
        options.addArguments("--window-size=1920,1080");
        driver = new ChromeDriver(options);
    }
 
    @Test
    public void shouldRunHeadlessAndAssertTitle() {
        driver.get("https://qa.codes");
        String title = driver.getTitle();
        Assert.assertTrue(title.contains("qa.codes"));
    }
 
    @AfterMethod
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

Run it from IntelliJ. No visible Chrome window appears, but the test passes. That's exactly what your CI server is going to do all day, every day.

Where Playwright wins on driver management

Honesty: Playwright's npx playwright install downloads pinned Chromium, Firefox, and WebKit binaries to a project-local cache, version-locked to the Playwright version. There's no version-mismatch class of bug at all. Selenium's drivers track the system browser, which is more flexible (you test against the Chrome version your users actually have) but exposes you to update churn.

Selenium Manager and WebDriverManager exist precisely to close that gap. They work — but the underlying complexity is real, and "ChromeDriver version mismatch" remains a top-3 search hit for Selenium issues.

The Selenium tool entry on qa.codes covers every driver-creation pattern referenced in this lesson.

⚠️ Common mistakes

  • Manually downloading ChromeDriver in 2026. You don't have to. WebDriverManager and Selenium Manager both handle it. If a tutorial has you putting chromedriver.exe on your PATH or setting System.setProperty("webdriver.chrome.driver", ...), that tutorial is from 2018 — skip the manual step and use the auto-resolver instead.
  • Using --headless (the old mode) on Chrome. It still works but produces subtly different rendering than real Chrome — fonts, scrollbars, and some CSS animations diverge. Use --headless=new everywhere; it's the modern path that matches real browser behaviour.
  • Forgetting the Linux-container flags. On a Docker-based CI runner, --no-sandbox and --disable-dev-shm-usage are nearly always required. The symptoms are cryptic — chrome failed to start or random crashes mid-test. If you see those on Linux/Docker, add the two flags before debugging anything else.

🎯 Practice task

Get headless and cross-browser running. 25–35 minutes.

  1. Add HeadlessChromeTest from this lesson to your project. Run it from IntelliJ. Confirm the test passes and no browser window appears.

  2. Create FirefoxHomeTest — same shape as HomePageTest from lesson 3, but with WebDriverManager.firefoxdriver().setup() and new FirefoxDriver(). (Install Firefox first if it isn't on your machine.) Run both browsers, confirm both pass.

  3. Open ~/.cache/selenium/ (Linux/macOS) or %USERPROFILE%\.cache\selenium\ (Windows). You should see folders for chromedriver and geckodriver with version-named subfolders inside. That's WebDriverManager's local cache — proof the binaries are there.

  4. Force a CI-shaped run. From the terminal, run CI=true mvn test -Dtest=HeadlessChromeTest (Linux/macOS) or set the env var via set CI=true on Windows. The test runs headless and the browser is invisible end to end. Now you've simulated what CI does.

  5. Try Selenium Manager. Comment out the WebDriverManager.chromedriver().setup(); line in HeadlessChromeTest. Run again. The test still passes — that's Selenium Manager picking up the slack. You've now seen both auto-resolvers do their job.

  6. Stretch: introduce a system-property-driven browser switch. Replace the hardcoded new ChromeDriver() with a small factory:

    String browser = System.getProperty("browser", "chrome");
    driver = switch (browser) {
        case "firefox" -> { WebDriverManager.firefoxdriver().setup(); yield new FirefoxDriver(); }
        case "edge"    -> { WebDriverManager.edgedriver().setup(); yield new EdgeDriver(); }
        default        -> { WebDriverManager.chromedriver().setup(); yield new ChromeDriver(); }
    };

    Run with mvn test -Dbrowser=firefox. The same test executes against Firefox. That's the foundation of cross-browser testing — we'll formalise it in chapter 7.

Chapter 1 is done. You can scaffold a Maven + Selenium + TestNG project, write a test, and run it across browsers locally and headless on CI. Chapter 2 is where the real Selenium skills start: locators — finding elements on a page reliably enough that your tests survive a year of UI churn.

// tip to track lessons you complete and pick up where you left off across devices.