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-based →
chromedriver - Firefox →
geckodriver - Edge →
msedgedriver - 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 setupWhen 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.exeon yourPATHor settingSystem.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=neweverywhere; it's the modern path that matches real browser behaviour. - Forgetting the Linux-container flags. On a Docker-based CI runner,
--no-sandboxand--disable-dev-shm-usageare nearly always required. The symptoms are cryptic —chrome failed to startor 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.
-
Add
HeadlessChromeTestfrom this lesson to your project. Run it from IntelliJ. Confirm the test passes and no browser window appears. -
Create
FirefoxHomeTest— same shape asHomePageTestfrom lesson 3, but withWebDriverManager.firefoxdriver().setup()andnew FirefoxDriver(). (Install Firefox first if it isn't on your machine.) Run both browsers, confirm both pass. -
Open
~/.cache/selenium/(Linux/macOS) or%USERPROFILE%\.cache\selenium\(Windows). You should see folders forchromedriverandgeckodriverwith version-named subfolders inside. That's WebDriverManager's local cache — proof the binaries are there. -
Force a CI-shaped run. From the terminal, run
CI=true mvn test -Dtest=HeadlessChromeTest(Linux/macOS) or set the env var viaset CI=trueon Windows. The test runs headless and the browser is invisible end to end. Now you've simulated what CI does. -
Try Selenium Manager. Comment out the
WebDriverManager.chromedriver().setup();line inHeadlessChromeTest. Run again. The test still passes — that's Selenium Manager picking up the slack. You've now seen both auto-resolvers do their job. -
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.